// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package bootstrap

import (
	"encoding/json"
	"fmt"
	"path/filepath"
	"runtime"
	"strings"

	"github.com/google/blueprint"
	"github.com/google/blueprint/depset"
	"github.com/google/blueprint/pathtools"
	"github.com/google/blueprint/proptools"
)

var (
	pctx = blueprint.NewPackageContext("github.com/google/blueprint/bootstrap")

	goTestMainCmd   = pctx.StaticVariable("goTestMainCmd", filepath.Join("$ToolDir", "gotestmain"))
	goTestRunnerCmd = pctx.StaticVariable("goTestRunnerCmd", filepath.Join("$ToolDir", "gotestrunner"))
	pluginGenSrcCmd = pctx.StaticVariable("pluginGenSrcCmd", filepath.Join("$ToolDir", "loadplugins"))
	gobGenCmd       = pctx.StaticVariable("gobGenCmd", filepath.Join("$ToolDir", "gob_gen"))

	parallelCompile = pctx.StaticVariable("parallelCompile", func() string {
		numCpu := runtime.NumCPU()
		// This will cause us to recompile all go programs if the
		// number of cpus changes. We don't get a lot of benefit from
		// higher values, so cap this to make it cheaper to move trees
		// between machines.
		if numCpu > 8 {
			numCpu = 8
		}
		return fmt.Sprintf("-c %d", numCpu)
	}())

	compile = pctx.StaticRule("compile",
		blueprint.RuleParams{
			Command: "GOROOT='$goRoot' $compileCmd $parallelCompile -o $out.tmp " +
				"$debugFlags -p $pkgPath -complete $incFlags $embedFlags -pack $in && " +
				"if cmp --quiet $out.tmp $out; then rm $out.tmp; else mv -f $out.tmp $out; fi",
			CommandDeps: []string{"$compileCmd"},
			Description: "compile $out",
			Restat:      true,
		},
		"pkgPath", "incFlags", "embedFlags")

	link = pctx.StaticRule("link",
		blueprint.RuleParams{
			Command: "GOROOT='$goRoot' $linkCmd -o $out.tmp $libDirFlags $in && " +
				"if cmp --quiet $out.tmp $out; then rm $out.tmp; else mv -f $out.tmp $out; fi",
			CommandDeps: []string{"$linkCmd"},
			Description: "link $out",
			Restat:      true,
		},
		"libDirFlags")

	goTestMain = pctx.StaticRule("gotestmain",
		blueprint.RuleParams{
			Command:     "$goTestMainCmd -o $out -pkg $pkg $in",
			CommandDeps: []string{"$goTestMainCmd"},
			Description: "gotestmain $out",
		},
		"pkg")

	pluginGenSrc = pctx.StaticRule("pluginGenSrc",
		blueprint.RuleParams{
			Command:     "$pluginGenSrcCmd -o $out -p $pkg $plugins",
			CommandDeps: []string{"$pluginGenSrcCmd"},
			Description: "create $out",
		},
		"pkg", "plugins")

	verifySerializers = pctx.StaticRule("generateSerializers",
		blueprint.RuleParams{
			Command:     "rm -f $out && $gobGenCmd -verify $in && touch $out",
			CommandDeps: []string{"$gobGenCmd"},
			Description: "generate serializers $out",
		})

	test = pctx.StaticRule("test",
		blueprint.RuleParams{
			Command:     "$goTestRunnerCmd -p $pkgSrcDir -f $out -- $in -test.short",
			CommandDeps: []string{"$goTestRunnerCmd"},
			Description: "test $pkg",
		},
		"pkg", "pkgSrcDir")

	cp = pctx.StaticRule("cp",
		blueprint.RuleParams{
			Command:     "cp $in $out",
			Description: "cp $out",
		},
		"generator")

	touch = pctx.StaticRule("touch",
		blueprint.RuleParams{
			Command:     "touch $out",
			Description: "touch $out",
		},
		"depfile", "generator")

	cat = pctx.StaticRule("Cat",
		blueprint.RuleParams{
			Command:     "rm -f $out && cat $in > $out",
			Description: "concatenate files to $out",
		})

	// ubuntu 14.04 offcially use dash for /bin/sh, and its builtin echo command
	// doesn't support -e option. Therefore we force to use /bin/bash when writing out
	// content to file.
	writeFile = pctx.StaticRule("writeFile",
		blueprint.RuleParams{
			Command:     `rm -f $out && /bin/bash -c 'echo -e -n "$$0" > $out' $content`,
			Description: "writing file $out",
		},
		"content")

	generateBuildNinja = pctx.StaticRule("build.ninja",
		blueprint.RuleParams{
			// TODO: it's kinda ugly that some parameters are computed from
			// environment variables and some from Ninja parameters, but it's probably
			// better to not to touch that while Blueprint and Soong are separate
			// NOTE: The spaces at EOL are important because otherwise Ninja would
			// omit all spaces between the different options.
			Command: `cd "$$(dirname "$builder")" && ` +
				`BUILDER="$$PWD/$$(basename "$builder")" && ` +
				`cd / && ` +
				`env -i $env "$$BUILDER" ` +
				`    --top "$$TOP" ` +
				`    --soong_out "$soongOutDir" ` +
				`    --out "$outDir" ` +
				`    $extra`,
			CommandDeps: []string{"$builder"},
			Description: "$builder $out",
			Deps:        blueprint.DepsGCC,
			Depfile:     "$out.d",
			Restat:      true,
		},
		"builder", "env", "extra", "pool")

	// Work around a Ninja issue.  See https://github.com/martine/ninja/pull/634
	phony = pctx.StaticRule("phony",
		blueprint.RuleParams{
			Command:     "# phony $out",
			Description: "phony $out",
			Generator:   true,
		},
		"depfile")

	_ = pctx.VariableFunc("ToolDir", func(ctx blueprint.VariableFuncContext, config interface{}) (string, error) {
		return config.(BootstrapConfig).HostToolDir(), nil
	})
)

var (
	// echoEscaper escapes a string such that passing it to "echo -e" will produce the input value.
	echoEscaper = strings.NewReplacer(
		`\`, `\\`, // First escape existing backslashes so they aren't interpreted by `echo -e`.
		"\n", `\n`, // Then replace newlines with \n
	)
)

// shardString takes a string and returns a slice of strings where the length of each one is
// at most shardSize.
func shardString(s string, shardSize int) []string {
	if len(s) == 0 {
		return nil
	}
	ret := make([]string, 0, (len(s)+shardSize-1)/shardSize)
	for len(s) > shardSize {
		ret = append(ret, s[0:shardSize])
		s = s[shardSize:]
	}
	if len(s) > 0 {
		ret = append(ret, s)
	}
	return ret
}

// writeFileRule creates a ninja rule to write contents to a file.  The contents will be
// escaped so that the file contains exactly the contents passed to the function.
func writeFileRule(ctx blueprint.ModuleContext, outputFile string, content string) {
	// This is MAX_ARG_STRLEN subtracted with some safety to account for shell escapes
	const SHARD_SIZE = 131072 - 10000

	buildWriteFileRule := func(outputFile string, content string) {
		content = echoEscaper.Replace(content)
		content = proptools.NinjaEscape(proptools.ShellEscapeIncludingSpaces(content))
		if content == "" {
			content = "''"
		}
		ctx.Build(pctx, blueprint.BuildParams{
			Rule:        writeFile,
			Outputs:     []string{outputFile},
			Description: "write " + outputFile,
			Args: map[string]string{
				"content": content,
			},
		})
	}

	if len(content) > SHARD_SIZE {
		var chunks []string
		for i, c := range shardString(content, SHARD_SIZE) {
			tempPath := fmt.Sprintf("%s.%d", outputFile, i)
			buildWriteFileRule(tempPath, c)
			chunks = append(chunks, tempPath)
		}
		ctx.Build(pctx, blueprint.BuildParams{
			Rule:        cat,
			Inputs:      chunks,
			Outputs:     []string{outputFile},
			Description: "Merging to " + outputFile,
		})
		return
	}
	buildWriteFileRule(outputFile, content)
}

type pluginDependencyTag struct {
	blueprint.BaseDependencyTag
}

func (pluginDependencyTag) ExcludeFromVisibilityEnforcement() {}

type bootstrapDependencies interface {
	bootstrapDeps(ctx blueprint.BottomUpMutatorContext)
}

var PluginDepTag = pluginDependencyTag{}

func BootstrapDeps(ctx blueprint.BottomUpMutatorContext) {
	if pkg, ok := ctx.Module().(bootstrapDependencies); ok {
		pkg.bootstrapDeps(ctx)
	}
}

type PackageInfo struct {
	PkgPath       string
	PkgRoot       string
	PackageTarget string
	TestTargets   []string

	TransitivePkgRoot       depset.DepSet[string]
	TransitivePackageTarget depset.DepSet[string]
}

var PackageProvider = blueprint.NewProvider[*PackageInfo]()

type BinaryInfo struct {
	IntermediatePath string
	InstallPath      string
	TestTargets      []string
}

var BinaryProvider = blueprint.NewProvider[*BinaryInfo]()

type DocsPackageInfo struct {
	PkgPath string
	Srcs    []string
}

var DocsPackageProvider = blueprint.NewMutatorProvider[*DocsPackageInfo]("bootstrap_deps")

// A GoPackage is a module for building Go packages.
type GoPackage struct {
	blueprint.ModuleBase
	blueprint.SimpleName
	properties struct {
		Deps      []string
		PkgPath   string
		Srcs      []string
		TestSrcs  []string
		TestData  []string
		PluginFor []string
		EmbedSrcs []string
		// The visibility property is unused in blueprint, but exists so that soong
		// can add one and not have the bp files fail to parse during the bootstrap build.
		Visibility []string

		Darwin struct {
			Srcs     []string
			TestSrcs []string
		}
		Linux struct {
			Srcs     []string
			TestSrcs []string
		}
	}
}

func newGoPackageModuleFactory() func() (blueprint.Module, []interface{}) {
	return func() (blueprint.Module, []interface{}) {
		module := &GoPackage{}
		return module, []interface{}{&module.properties, &module.SimpleName.Properties}
	}
}

// Properties returns the list of property structs to be used for registering a wrapped module type.
func (g *GoPackage) Properties() []interface{} {
	return []interface{}{&g.properties}
}

func (g *GoPackage) DynamicDependencies(ctx blueprint.DynamicDependerModuleContext) []string {
	return g.properties.Deps
}

func (g *GoPackage) bootstrapDeps(ctx blueprint.BottomUpMutatorContext) {
	for _, plugin := range g.properties.PluginFor {
		ctx.AddReverseDependency(ctx.Module(), PluginDepTag, plugin)
	}
	blueprint.SetProvider(ctx, DocsPackageProvider, &DocsPackageInfo{
		PkgPath: g.properties.PkgPath,
		Srcs:    g.properties.Srcs,
	})
}

func (g *GoPackage) GenerateBuildActions(ctx blueprint.ModuleContext) {
	var (
		name       = ctx.ModuleName()
		hasPlugins = false
		pluginSrc  = ""
		genSrcs    = []string{}
	)

	if g.properties.PkgPath == "" {
		ctx.ModuleErrorf("module %s did not specify a valid pkgPath", name)
		return
	}

	pkgRoot := packageRoot(ctx)
	archiveFile := filepath.Join(pkgRoot,
		filepath.FromSlash(g.properties.PkgPath)+".a")

	ctx.VisitDirectDepsProxy(func(module blueprint.ModuleProxy) {
		if ctx.OtherModuleDependencyTag(module) == PluginDepTag {
			hasPlugins = true
		}
	})
	if hasPlugins {
		pluginSrc = filepath.Join(moduleGenSrcDir(ctx), "plugin.go")
		genSrcs = append(genSrcs, pluginSrc)
	}

	if hasPlugins && !buildGoPluginLoader(ctx, g.properties.PkgPath, pluginSrc) {
		return
	}

	var srcs, testSrcs []string
	if runtime.GOOS == "darwin" {
		srcs = append(g.properties.Srcs, g.properties.Darwin.Srcs...)
		testSrcs = append(g.properties.TestSrcs, g.properties.Darwin.TestSrcs...)
	} else if runtime.GOOS == "linux" {
		srcs = append(g.properties.Srcs, g.properties.Linux.Srcs...)
		testSrcs = append(g.properties.TestSrcs, g.properties.Linux.TestSrcs...)
	}

	testArchiveFile := filepath.Join(testRoot(ctx),
		filepath.FromSlash(g.properties.PkgPath)+".a")
	testResultFile := buildGoTest(ctx, testRoot(ctx), testArchiveFile,
		g.properties.PkgPath, srcs, genSrcs, testSrcs, g.properties.EmbedSrcs)

	// Don't build for test-only packages
	if len(srcs) == 0 && len(genSrcs) == 0 {
		ctx.Build(pctx, blueprint.BuildParams{
			Rule:    touch,
			Outputs: []string{archiveFile},
		})
		return
	}

	incFlags, deps, _, _, transitivePkgRoot, transitivePackageTarget := collectGoDeps(ctx)

	buildGoPackage(ctx, pkgRoot, g.properties.PkgPath, archiveFile,
		srcs, genSrcs, g.properties.EmbedSrcs, incFlags, deps)

	blueprint.SetProvider(ctx, PackageProvider, &PackageInfo{
		PkgPath:       g.properties.PkgPath,
		PkgRoot:       pkgRoot,
		PackageTarget: archiveFile,
		TestTargets:   testResultFile,

		TransitivePkgRoot:       transitivePkgRoot,
		TransitivePackageTarget: transitivePackageTarget,
	})
}

func collectGoDeps(ctx blueprint.ModuleContext) (incFlags, incFlagsDeps, linkFlags, linkFlagsDeps []string,
	transitivePkgRoot, transitivePackageTarget depset.DepSet[string]) {

	transitivePkgRootBuilder := depset.NewBuilder[string](depset.POSTORDER)
	transitivePackageTargetBuilder := depset.NewBuilder[string](depset.POSTORDER)

	ctx.VisitDirectDepsProxy(func(module blueprint.ModuleProxy) {
		if info, ok := blueprint.OtherModuleProvider(ctx, module, PackageProvider); ok {
			incDir := info.PkgRoot
			target := info.PackageTarget
			incFlags = append(incFlags, "-I "+incDir)
			incFlagsDeps = append(incFlagsDeps, target)
			transitivePkgRootBuilder.Direct(info.PkgRoot).Transitive(info.TransitivePkgRoot)
			transitivePackageTargetBuilder.Direct(info.PackageTarget).Transitive(info.TransitivePackageTarget)
		}
	})

	transitivePkgRoot = transitivePkgRootBuilder.Build()
	transitivePackageTarget = transitivePackageTargetBuilder.Build()

	linkFlagsDeps = transitivePackageTarget.ToList()
	for _, libDir := range transitivePkgRoot.ToList() {
		linkFlags = append(linkFlags, "-L "+libDir)
	}
	return
}

func buildVerifySerializers(ctx blueprint.ModuleContext, outputFile string, srcs []string) {
	var srcPaths []string
	for _, src := range srcs {
		srcPaths = append(srcPaths, filepath.Join(moduleSrcDir(ctx), src))
	}

	ctx.Build(pctx, blueprint.BuildParams{
		Rule:    verifySerializers,
		Inputs:  srcPaths,
		Outputs: []string{outputFile},
	})
}

// A GoBinary is a module for building executable binaries from Go sources.
type GoBinary struct {
	blueprint.ModuleBase
	blueprint.SimpleName
	properties struct {
		Deps           []string
		Srcs           []string
		TestSrcs       []string
		TestData       []string
		EmbedSrcs      []string
		PrimaryBuilder bool
		Default        bool
		// The visibility property is unused in blueprint, but exists so that soong
		// can add one and not have the bp files fail to parse during the bootstrap build.
		Visibility []string

		Darwin struct {
			Srcs     []string
			TestSrcs []string
		}
		Linux struct {
			Srcs     []string
			TestSrcs []string
		}
	}

	installPath string

	// skipInstall can be set to true by a module type that wraps GoBinary to skip the install rule,
	// allowing the wrapping module type to create the install rule itself.
	skipInstall bool

	// outputFile is set to the path to the intermediate output file.
	outputFile string
}

func newGoBinaryModuleFactory() func() (blueprint.Module, []interface{}) {
	return func() (blueprint.Module, []interface{}) {
		module := &GoBinary{}
		return module, []interface{}{&module.properties, &module.SimpleName.Properties}
	}
}

func (g *GoBinary) DynamicDependencies(ctx blueprint.DynamicDependerModuleContext) []string {
	return g.properties.Deps
}

func (g *GoBinary) bootstrapDeps(ctx blueprint.BottomUpMutatorContext) {
	if g.properties.PrimaryBuilder {
		blueprint.SetProvider(ctx, PrimaryBuilderProvider, PrimaryBuilderInfo{})
	}
}

// IntermediateFile returns the path to the final linked intermedate file.
func (g *GoBinary) IntermediateFile() string {
	return g.outputFile
}

// SetSkipInstall is called by module types that wrap GoBinary to skip the install rule,
// allowing the wrapping module type to create the install rule itself.
func (g *GoBinary) SetSkipInstall() {
	g.skipInstall = true
}

// Properties returns the list of property structs to be used for registering a wrapped module type.
func (g *GoBinary) Properties() []interface{} {
	return []interface{}{&g.properties}
}

func (g *GoBinary) GenerateBuildActions(ctx blueprint.ModuleContext) {
	var (
		name            = ctx.ModuleName()
		objDir          = packageRoot(ctx)
		archiveFile     = filepath.Join(objDir, name+".a")
		testArchiveFile = filepath.Join(testRoot(ctx), name+".a")
		aoutFile        = filepath.Join(objDir, name)
		hasPlugins      = false
		pluginSrc       = ""
		genSrcs         = []string{}
	)

	if !g.skipInstall {
		g.installPath = filepath.Join(ctx.Config().(BootstrapConfig).HostToolDir(), name)
	}

	ctx.VisitDirectDepsProxy(func(module blueprint.ModuleProxy) {
		if ctx.OtherModuleDependencyTag(module) == PluginDepTag {
			hasPlugins = true
		}
	})
	if hasPlugins {
		pluginSrc = filepath.Join(moduleGenSrcDir(ctx), "plugin.go")
		genSrcs = append(genSrcs, pluginSrc)
	}

	if hasPlugins && !buildGoPluginLoader(ctx, "main", pluginSrc) {
		return
	}

	var srcs, testSrcs []string
	if runtime.GOOS == "darwin" {
		srcs = append(g.properties.Srcs, g.properties.Darwin.Srcs...)
		testSrcs = append(g.properties.TestSrcs, g.properties.Darwin.TestSrcs...)
	} else if runtime.GOOS == "linux" {
		srcs = append(g.properties.Srcs, g.properties.Linux.Srcs...)
		testSrcs = append(g.properties.TestSrcs, g.properties.Linux.TestSrcs...)
	}

	testResultFile := buildGoTest(ctx, testRoot(ctx), testArchiveFile,
		name, srcs, genSrcs, testSrcs, g.properties.EmbedSrcs)

	incFlags, deps, linkFlags, linkFlagsDeps, _, _ := collectGoDeps(ctx)

	buildGoPackage(ctx, objDir, "main", archiveFile, srcs, genSrcs, g.properties.EmbedSrcs, incFlags, deps)

	linkArgs := map[string]string{}
	if len(linkFlags) > 0 {
		linkArgs["libDirFlags"] = strings.Join(linkFlags, " ")
	}

	ctx.Build(pctx, blueprint.BuildParams{
		Rule:      link,
		Outputs:   []string{aoutFile},
		Inputs:    []string{archiveFile},
		Implicits: linkFlagsDeps,
		Args:      linkArgs,
	})

	g.outputFile = aoutFile

	var validations []string
	if ctx.Config().(BootstrapConfig).RunGoTests() {
		validations = testResultFile
	}

	if !g.skipInstall {
		ctx.Build(pctx, blueprint.BuildParams{
			Rule:        cp,
			Outputs:     []string{g.installPath},
			Inputs:      []string{aoutFile},
			Validations: validations,
			Default:     g.properties.Default,
		})
	}

	blueprint.SetProvider(ctx, BinaryProvider, &BinaryInfo{
		IntermediatePath: g.outputFile,
		InstallPath:      g.installPath,
		TestTargets:      testResultFile,
	})
}

func buildGoPluginLoader(ctx blueprint.ModuleContext, pkgPath, pluginSrc string) bool {
	ret := true

	var pluginPaths []string
	ctx.VisitDirectDepsProxy(func(module blueprint.ModuleProxy) {
		if ctx.OtherModuleDependencyTag(module) == PluginDepTag {
			if info, ok := blueprint.OtherModuleProvider(ctx, module, PackageProvider); ok {
				pluginPaths = append(pluginPaths, info.PkgPath)
			}
		}
	})

	ctx.Build(pctx, blueprint.BuildParams{
		Rule:    pluginGenSrc,
		Outputs: []string{pluginSrc},
		Args: map[string]string{
			"pkg":     pkgPath,
			"plugins": strings.Join(pluginPaths, " "),
		},
	})

	return ret
}

func generateEmbedcfgFile(ctx blueprint.ModuleContext, files []string, srcDir string, embedcfgFile string) {
	embedcfg := struct {
		Patterns map[string][]string
		Files    map[string]string
	}{
		make(map[string][]string, len(files)),
		make(map[string]string, len(files)),
	}

	for _, file := range files {
		embedcfg.Patterns[file] = []string{file}
		embedcfg.Files[file] = filepath.Join(srcDir, file)
	}

	embedcfgData, err := json.Marshal(&embedcfg)
	if err != nil {
		ctx.ModuleErrorf("Failed to marshal embedcfg data: %s", err.Error())
	}

	writeFileRule(ctx, embedcfgFile, string(embedcfgData))
}

func buildGoPackage(ctx blueprint.ModuleContext, pkgRoot string,
	pkgPath string, archiveFile string, srcs []string, genSrcs []string, embedSrcs []string,
	incFlags []string, deps []string) {

	srcDir := moduleSrcDir(ctx)
	srcFiles := pathtools.PrefixPaths(srcs, srcDir)
	srcFiles = append(srcFiles, genSrcs...)

	compileArgs := map[string]string{
		"pkgPath": pkgPath,
	}

	if len(incFlags) > 0 {
		compileArgs["incFlags"] = strings.Join(incFlags, " ")
	}

	if len(embedSrcs) > 0 {
		embedcfgFile := archiveFile + ".embedcfg"
		generateEmbedcfgFile(ctx, embedSrcs, srcDir, embedcfgFile)
		compileArgs["embedFlags"] = "-embedcfg " + embedcfgFile
		deps = append(deps, embedcfgFile)
	}

	var validations []string

	if ctx.Config().(BootstrapConfig).IsBootstrap() {
		verifySerializers := archiveFile + ".verify_serializers"
		buildVerifySerializers(ctx, verifySerializers, srcs)
		validations = append(validations, verifySerializers)
	}

	ctx.Build(pctx, blueprint.BuildParams{
		Rule:        compile,
		Outputs:     []string{archiveFile},
		Inputs:      srcFiles,
		Implicits:   deps,
		Args:        compileArgs,
		Validations: validations,
	})
}

func buildGoTest(ctx blueprint.ModuleContext, testRoot, testPkgArchive,
	pkgPath string, srcs, genSrcs, testSrcs []string, embedSrcs []string) []string {

	var testDeps []string
	ctx.VisitDirectDepsProxy(func(module blueprint.ModuleProxy) {
		if info, ok := blueprint.OtherModuleProvider(ctx, module, PackageProvider); ok {
			testDeps = append(testDeps, info.TestTargets...)
		}
	})

	if len(testSrcs) == 0 {
		if len(testDeps) > 0 {
			testDependencies := filepath.Join(testRoot, "test.dependencies")
			ctx.Build(pctx, blueprint.BuildParams{
				Rule:        touch,
				Outputs:     []string{testDependencies},
				Validations: testDeps,
			})
			return []string{testDependencies}
		}
		return nil
	}

	srcDir := moduleSrcDir(ctx)
	testFiles := pathtools.PrefixPaths(testSrcs, srcDir)

	mainFile := filepath.Join(testRoot, "test.go")
	testArchive := filepath.Join(testRoot, "test.a")
	testFile := filepath.Join(testRoot, "test")
	testPassed := filepath.Join(testRoot, "test.passed")

	incFlags, deps, linkFlags, linkFlagsDeps, _, _ := collectGoDeps(ctx)

	buildGoPackage(ctx, testRoot, pkgPath, testPkgArchive,
		append(srcs, testSrcs...), genSrcs, embedSrcs, incFlags, deps)

	ctx.Build(pctx, blueprint.BuildParams{
		Rule:    goTestMain,
		Outputs: []string{mainFile},
		Inputs:  testFiles,
		Args: map[string]string{
			"pkg": pkgPath,
		},
	})

	linkFlagsDeps = append([]string{testPkgArchive}, linkFlagsDeps...)
	linkFlags = append([]string{"-L " + testRoot}, linkFlags...)

	ctx.Build(pctx, blueprint.BuildParams{
		Rule:      compile,
		Outputs:   []string{testArchive},
		Inputs:    []string{mainFile},
		Implicits: []string{testPkgArchive},
		Args: map[string]string{
			"pkgPath":  "main",
			"incFlags": "-I " + testRoot,
		},
	})

	ctx.Build(pctx, blueprint.BuildParams{
		Rule:      link,
		Outputs:   []string{testFile},
		Inputs:    []string{testArchive},
		Implicits: linkFlagsDeps,
		Args: map[string]string{
			"libDirFlags": strings.Join(linkFlags, " "),
		},
	})

	ctx.Build(pctx, blueprint.BuildParams{
		Rule:        test,
		Outputs:     []string{testPassed},
		Inputs:      []string{testFile},
		Validations: testDeps,
		Args: map[string]string{
			"pkg":       pkgPath,
			"pkgSrcDir": filepath.Dir(testFiles[0]),
		},
	})

	return []string{testPassed}
}

var PrimaryBuilderProvider = blueprint.NewMutatorProvider[PrimaryBuilderInfo]("bootstrap_deps")

type PrimaryBuilderInfo struct{}

type singleton struct {
}

func newSingletonFactory() func() blueprint.Singleton {
	return func() blueprint.Singleton {
		return &singleton{}
	}
}

func (s *singleton) GenerateBuildActions(ctx blueprint.SingletonContext) {
	// Find the module that's marked as the "primary builder", which means it's
	// creating the binary that we'll use to generate the non-bootstrap
	// build.ninja file.
	var primaryBuilders []string
	// blueprintTools contains blueprint go binaries that will be built in StageMain
	var blueprintTools []string
	// blueprintTools contains the test outputs of go tests that can be run in StageMain
	var blueprintTests []string
	// blueprintGoPackages contains all blueprint go packages that can be built in StageMain
	var blueprintGoPackages []string
	ctx.VisitAllModuleProxies(func(module blueprint.ModuleProxy) {
		if ctx.IsPrimaryModule(module) {
			if binaryInfo, ok := blueprint.SingletonModuleProvider(ctx, module, BinaryProvider); ok {
				if binaryInfo.InstallPath != "" {
					blueprintTools = append(blueprintTools, binaryInfo.InstallPath)
				}
				blueprintTests = append(blueprintTests, binaryInfo.TestTargets...)
				if _, ok := blueprint.SingletonModuleProvider(ctx, module, PrimaryBuilderProvider); ok {
					primaryBuilders = append(primaryBuilders, binaryInfo.InstallPath)
				}
			}

			if packageInfo, ok := blueprint.SingletonModuleProvider(ctx, module, PackageProvider); ok {
				blueprintGoPackages = append(blueprintGoPackages, packageInfo.PackageTarget)
				blueprintTests = append(blueprintTests, packageInfo.TestTargets...)
			}
		}
	})

	var primaryBuilderCmdlinePrefix []string
	var primaryBuilderFile string

	if len(primaryBuilders) == 0 {
		ctx.Errorf("no primary builder module present")
		return
	} else if len(primaryBuilders) > 1 {
		ctx.Errorf("multiple primary builder modules present: %q", primaryBuilders)
		return
	} else {
		primaryBuilderFile = primaryBuilders[0]
	}

	ctx.SetOutDir(pctx, "${outDir}")

	for _, subninja := range ctx.Config().(BootstrapConfig).Subninjas() {
		ctx.AddSubninja(subninja)
	}

	for _, i := range ctx.Config().(BootstrapConfig).PrimaryBuilderInvocations() {
		flags := make([]string, 0)
		flags = append(flags, primaryBuilderCmdlinePrefix...)
		flags = append(flags, i.Args...)

		pool := ""
		if i.Console {
			pool = "console"
		}

		envAssignments := ""
		for k, v := range i.Env {
			// NB: This is rife with quoting issues but we don't care because we trust
			// soong_ui to not abuse this facility too much
			envAssignments += k + "=" + v + " "
		}

		// Build the main build.ninja
		ctx.Build(pctx, blueprint.BuildParams{
			Rule:      generateBuildNinja,
			Outputs:   i.Outputs,
			Inputs:    i.Inputs,
			Implicits: i.Implicits,
			OrderOnly: i.OrderOnlyInputs,
			Args: map[string]string{
				"builder": primaryBuilderFile,
				"env":     envAssignments,
				"extra":   strings.Join(flags, " "),
				"pool":    pool,
			},
			Description: i.Description,
		})
	}

	// Add a phony target for building various tools that are part of blueprint
	if len(blueprintTools) > 0 {
		ctx.Build(pctx, blueprint.BuildParams{
			Rule:    blueprint.Phony,
			Outputs: []string{"blueprint_tools"},
			Inputs:  blueprintTools,
			Default: true,
		})
	}

	// Add a phony target for running various tests that are part of blueprint
	ctx.Build(pctx, blueprint.BuildParams{
		Rule:    blueprint.Phony,
		Outputs: []string{"blueprint_tests"},
		Inputs:  blueprintTests,
		Default: true,
	})

	// Add a phony target for running go tests
	ctx.Build(pctx, blueprint.BuildParams{
		Rule:    blueprint.Phony,
		Outputs: []string{"blueprint_go_packages"},
		Inputs:  blueprintGoPackages,
	})
}

func (s *singleton) IncrementalSupported() bool {
	return false
}

// packageRoot returns the module-specific package root directory path.  This
// directory is where the final package .a files are output and where dependant
// modules search for this package via -I arguments.
func packageRoot(ctx blueprint.ModuleContext) string {
	toolDir := ctx.Config().(BootstrapConfig).HostToolDir()
	return filepath.Join(toolDir, "go", ctx.ModuleName(), ctx.ModuleSubDir(), "pkg")
}

// testRoot returns the module-specific package root directory path used for
// building tests. The .a files generated here will include everything from
// packageRoot, plus the test-only code.
func testRoot(ctx blueprint.ModuleContext) string {
	toolDir := ctx.Config().(BootstrapConfig).HostToolDir()
	return filepath.Join(toolDir, "go", ctx.ModuleName(), ctx.ModuleSubDir(), "test")
}

// moduleSrcDir returns the path of the directory that all source file paths are
// specified relative to.
func moduleSrcDir(ctx blueprint.ModuleContext) string {
	return ctx.ModuleDir()
}

// moduleGenSrcDir returns the module-specific generated sources path.
func moduleGenSrcDir(ctx blueprint.ModuleContext) string {
	toolDir := ctx.Config().(BootstrapConfig).HostToolDir()
	return filepath.Join(toolDir, "go", ctx.ModuleName(), ctx.ModuleSubDir(), "gen")
}
