Initial Blueprint commit.
Blueprint is a build system component that reads Blueprints files defining
modules to be built, and generates a Ninja build manifest that can be used to
perform all the build actions. It does not dictate or implement much build
policy itself, but rather provides a framework to ease the process of defining
build logic in Go.
The "blueprint" and "blueprint/parser" Go packages contain the functionality
for reading Blueprint files and invoking build logic functions defined in other
Go packages.
The "blueprint/bootstrap" Go package contains just enough build logic to build
a binary that includes Blueprint and any pure-Go (i.e. no cgo) build logic
defined in external Go packages. This can be used to create a minimal Ninja
file that's capable of bootstrapping a Blueprint-based build system from
source.
The "blueprint/bootstrap/minibp" Go package contains code for a minimal binary
that includes the build logic defined in the "blueprint/bootstrap" package.
This binary can then create the Ninja file for the bootstrapping process.
Change-Id: I8d8390042372a72d225785cda738525001b009f1
diff --git a/Blueprints b/Blueprints
new file mode 100644
index 0000000..5515e18
--- /dev/null
+++ b/Blueprints
@@ -0,0 +1,38 @@
+bootstrap_go_package {
+ name: "blueprint",
+ deps: ["blueprint-parser"],
+ pkgPath: "blueprint",
+ srcs: ["blueprint/context.go",
+ "blueprint/globals.go",
+ "blueprint/live_tracker.go",
+ "blueprint/mangle.go",
+ "blueprint/module_ctx.go",
+ "blueprint/ninja_defs.go",
+ "blueprint/ninja_strings.go",
+ "blueprint/ninja_writer.go",
+ "blueprint/scope.go",
+ "blueprint/singleton_ctx.go",
+ "blueprint/unpack.go"],
+}
+
+bootstrap_go_package {
+ name: "blueprint-parser",
+ pkgPath: "blueprint/parser",
+ srcs: ["blueprint/parser/parser.go"],
+}
+
+bootstrap_go_package {
+ name: "blueprint-bootstrap",
+ deps: ["blueprint"],
+ pkgPath: "blueprint/bootstrap",
+ srcs: ["blueprint/bootstrap/bootstrap.go",
+ "blueprint/bootstrap/command.go",
+ "blueprint/bootstrap/config.go",
+ "blueprint/bootstrap/doc.go"],
+}
+
+bootstrap_go_binary {
+ name: "minibp",
+ deps: ["blueprint", "blueprint-bootstrap"],
+ srcs: ["blueprint/bootstrap/minibp/main.go"],
+}
\ No newline at end of file
diff --git a/blueprint/bootstrap/bootstrap.go b/blueprint/bootstrap/bootstrap.go
new file mode 100644
index 0000000..5912524
--- /dev/null
+++ b/blueprint/bootstrap/bootstrap.go
@@ -0,0 +1,490 @@
+package bootstrap
+
+import (
+ "blueprint"
+ "fmt"
+ "path/filepath"
+ "strings"
+)
+
+var (
+ gcCmd = blueprint.StaticVariable("gcCmd", "$goToolDir/${GoChar}g")
+ packCmd = blueprint.StaticVariable("packCmd", "$goToolDir/pack")
+ linkCmd = blueprint.StaticVariable("linkCmd", "$goToolDir/${GoChar}l")
+
+ gc = blueprint.StaticRule("gc",
+ blueprint.RuleParams{
+ Command: "GOROOT='$GoRoot' $gcCmd -o $out -p $pkgPath -complete " +
+ "$incFlags $in",
+ Description: "${GoChar}g $out",
+ },
+ "pkgPath", "incFlags")
+
+ pack = blueprint.StaticRule("pack",
+ blueprint.RuleParams{
+ Command: "GOROOT='$GoRoot' $packCmd grcP $prefix $out $in",
+ Description: "pack $out",
+ },
+ "prefix")
+
+ link = blueprint.StaticRule("link",
+ blueprint.RuleParams{
+ Command: "GOROOT='$GoRoot' $linkCmd -o $out $libDirFlags $in",
+ Description: "${GoChar}l $out",
+ },
+ "libDirFlags")
+
+ cp = blueprint.StaticRule("cp",
+ blueprint.RuleParams{
+ Command: "cp $in $out",
+ Description: "cp $out",
+ })
+
+ bootstrap = blueprint.StaticRule("bootstrap",
+ blueprint.RuleParams{
+ Command: "$Bootstrap $in $BootstrapManifest",
+ Description: "bootstrap $in",
+ Generator: true,
+ })
+
+ rebootstrap = blueprint.StaticRule("rebootstrap",
+ blueprint.RuleParams{
+ // Ninja only re-invokes itself once when it regenerates a .ninja
+ // file. For the re-bootstrap process we need that to happen twice,
+ // so we invoke ninja ourselves once from this. Unfortunately this
+ // seems to cause "warning: bad deps log signature or version;
+ // starting over" messages from Ninja. This warning can be avoided
+ // by having the bootstrap and non-bootstrap build manifests have a
+ // different builddir (so they use different log files).
+ //
+ // This workaround can be avoided entirely by making a simple change
+ // to Ninja that would allow it to rebuild the manifest twice rather
+ // than just once.
+ Command: "$Bootstrap $in && ninja",
+ Description: "re-bootstrap $in",
+ Generator: true,
+ })
+
+ minibp = blueprint.StaticRule("minibp",
+ blueprint.RuleParams{
+ Command: fmt.Sprintf("%s -d %s -o $out $in",
+ minibpFile, minibpDepFile),
+ Description: "minibp $out",
+ Generator: true,
+ Restat: true,
+ Depfile: minibpDepFile,
+ Deps: blueprint.DepsGCC,
+ })
+
+ // Work around a Ninja issue. See https://github.com/martine/ninja/pull/634
+ phony = blueprint.StaticRule("phony",
+ blueprint.RuleParams{
+ Command: "# phony $out",
+ Description: "phony $out",
+ Generator: true,
+ },
+ "depfile")
+
+ goPackageModule = blueprint.MakeModuleType("goPackageModule", newGoPackage)
+ goBinaryModule = blueprint.MakeModuleType("goBinaryModule", newGoBinary)
+
+ binDir = filepath.Join("bootstrap", "bin")
+ minibpFile = filepath.Join(binDir, "minibp")
+ minibpDepFile = filepath.Join("bootstrap", "bootstrap_manifest.d")
+)
+
+type goPackageProducer interface {
+ GoPkgRoot() string
+ GoPackageTarget() string
+}
+
+func isGoPackageProducer(module blueprint.Module) bool {
+ _, ok := module.(goPackageProducer)
+ return ok
+}
+
+func isBootstrapModule(module blueprint.Module) bool {
+ _, isPackage := module.(*goPackage)
+ _, isBinary := module.(*goBinary)
+ return isPackage || isBinary
+}
+
+func isBootstrapBinaryModule(module blueprint.Module) bool {
+ _, isBinary := module.(*goBinary)
+ return isBinary
+}
+
+func generatingBootstrapper(config blueprint.Config) bool {
+ bootstrapConfig, ok := config.(Config)
+ if ok {
+ return bootstrapConfig.GeneratingBootstrapper()
+ }
+ return false
+}
+
+// A goPackage is a module for building Go packages.
+type goPackage struct {
+ properties struct {
+ PkgPath string
+ Srcs []string
+ }
+
+ // The root dir in which the package .a file is located. The full .a file
+ // path will be "packageRoot/PkgPath.a"
+ pkgRoot string
+
+ // The path of the .a file that is to be built.
+ archiveFile string
+}
+
+var _ goPackageProducer = (*goPackage)(nil)
+
+func newGoPackage() (blueprint.Module, interface{}) {
+ module := &goPackage{}
+ return module, &module.properties
+}
+
+func (g *goPackage) GoPkgRoot() string {
+ return g.pkgRoot
+}
+
+func (g *goPackage) GoPackageTarget() string {
+ return g.archiveFile
+}
+
+func (g *goPackage) GenerateBuildActions(ctx blueprint.ModuleContext) {
+ name := ctx.ModuleName()
+
+ if g.properties.PkgPath == "" {
+ ctx.ModuleErrorf("module %s did not specify a valid pkgPath", name)
+ return
+ }
+
+ g.pkgRoot = packageRoot(ctx)
+ g.archiveFile = filepath.Clean(filepath.Join(g.pkgRoot,
+ filepath.FromSlash(g.properties.PkgPath)+".a"))
+
+ // We only actually want to build the builder modules if we're running as
+ // minibp (i.e. we're generating a bootstrap Ninja file). This is to break
+ // the circular dependence that occurs when the builder requires a new Ninja
+ // file to be built, but building a new ninja file requires the builder to
+ // be built.
+ if generatingBootstrapper(ctx.Config()) {
+ buildGoPackage(ctx, g.pkgRoot, g.properties.PkgPath, g.archiveFile,
+ g.properties.Srcs)
+ } else {
+ phonyGoTarget(ctx, g.archiveFile, g.properties.Srcs)
+ }
+}
+
+// A goBinary is a module for building executable binaries from Go sources.
+type goBinary struct {
+ properties struct {
+ Srcs []string
+ PrimaryBuilder bool
+ }
+}
+
+func newGoBinary() (blueprint.Module, interface{}) {
+ module := &goBinary{}
+ return module, &module.properties
+}
+
+func (g *goBinary) GenerateBuildActions(ctx blueprint.ModuleContext) {
+ var (
+ name = ctx.ModuleName()
+ objDir = objDir(ctx)
+ archiveFile = filepath.Join(objDir, name+".a")
+ aoutFile = filepath.Join(objDir, "a.out")
+ binaryFile = filepath.Join(binDir, name)
+ )
+
+ // We only actually want to build the builder modules if we're running as
+ // minibp (i.e. we're generating a bootstrap Ninja file). This is to break
+ // the circular dependence that occurs when the builder requires a new Ninja
+ // file to be built, but building a new ninja file requires the builder to
+ // be built.
+ if generatingBootstrapper(ctx.Config()) {
+ buildGoPackage(ctx, objDir, name, archiveFile, g.properties.Srcs)
+
+ var libDirFlags []string
+ ctx.VisitDepsDepthFirstIf(isGoPackageProducer,
+ func(module blueprint.Module) {
+ dep := module.(goPackageProducer)
+ libDir := dep.GoPkgRoot()
+ libDirFlags = append(libDirFlags, "-L "+libDir)
+ })
+
+ linkArgs := map[string]string{}
+ if len(libDirFlags) > 0 {
+ linkArgs["libDirFlags"] = strings.Join(libDirFlags, " ")
+ }
+
+ ctx.Build(blueprint.BuildParams{
+ Rule: link,
+ Outputs: []string{aoutFile},
+ Inputs: []string{archiveFile},
+ Args: linkArgs,
+ })
+
+ ctx.Build(blueprint.BuildParams{
+ Rule: cp,
+ Outputs: []string{binaryFile},
+ Inputs: []string{aoutFile},
+ })
+ } else {
+ phonyGoTarget(ctx, binaryFile, g.properties.Srcs)
+ }
+}
+
+func buildGoPackage(ctx blueprint.ModuleContext, pkgRoot string,
+ pkgPath string, archiveFile string, srcs []string) {
+
+ srcDir := srcDir(ctx)
+ srcFiles := PrefixPaths(srcs, srcDir)
+
+ objDir := objDir(ctx)
+ objFile := filepath.Join(objDir, "_go_.$GoChar")
+
+ var incFlags []string
+ var depTargets []string
+ ctx.VisitDepsDepthFirstIf(isGoPackageProducer,
+ func(module blueprint.Module) {
+ dep := module.(goPackageProducer)
+ incDir := dep.GoPkgRoot()
+ target := dep.GoPackageTarget()
+ incFlags = append(incFlags, "-I "+incDir)
+ depTargets = append(depTargets, target)
+ })
+
+ gcArgs := map[string]string{
+ "pkgPath": pkgPath,
+ }
+
+ if len(incFlags) > 0 {
+ gcArgs["incFlags"] = strings.Join(incFlags, " ")
+ }
+
+ ctx.Build(blueprint.BuildParams{
+ Rule: gc,
+ Outputs: []string{objFile},
+ Inputs: srcFiles,
+ Implicits: depTargets,
+ Args: gcArgs,
+ })
+
+ ctx.Build(blueprint.BuildParams{
+ Rule: pack,
+ Outputs: []string{archiveFile},
+ Inputs: []string{objFile},
+ Args: map[string]string{
+ "prefix": pkgRoot,
+ },
+ })
+}
+
+func phonyGoTarget(ctx blueprint.ModuleContext, target string, srcs []string) {
+ var depTargets []string
+ ctx.VisitDepsDepthFirstIf(isGoPackageProducer,
+ func(module blueprint.Module) {
+ dep := module.(goPackageProducer)
+ target := dep.GoPackageTarget()
+ depTargets = append(depTargets, target)
+ })
+
+ moduleDir := ctx.ModuleDir()
+ srcs = PrefixPaths(srcs, filepath.Join("$SrcDir", moduleDir))
+
+ ctx.Build(blueprint.BuildParams{
+ Rule: phony,
+ Outputs: []string{target},
+ Inputs: srcs,
+ Implicits: depTargets,
+ })
+
+ // If one of the source files gets deleted or renamed that will prevent the
+ // re-bootstrapping happening because it depends on the missing source file.
+ // To get around this we add a build statement using the built-in phony rule
+ // for each source file, which will cause Ninja to treat it as dirty if its
+ // missing.
+ for _, src := range srcs {
+ ctx.Build(blueprint.BuildParams{
+ Rule: blueprint.Phony,
+ Outputs: []string{src},
+ })
+ }
+}
+
+type singleton struct{}
+
+func newSingleton() *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 []*goBinary
+ ctx.VisitAllModulesIf(isBootstrapBinaryModule,
+ func(module blueprint.Module) {
+ binaryModule := module.(*goBinary)
+ if binaryModule.properties.PrimaryBuilder {
+ primaryBuilders = append(primaryBuilders, binaryModule)
+ }
+ })
+
+ var primaryBuilderName, primaryBuilderExtraFlags string
+ switch len(primaryBuilders) {
+ case 0:
+ // If there's no primary builder module then that means we'll use minibp
+ // as the primary builder. We can trigger its primary builder mode with
+ // the -p flag.
+ primaryBuilderName = "minibp"
+ primaryBuilderExtraFlags = "-p"
+
+ case 1:
+ primaryBuilderName = ctx.ModuleName(primaryBuilders[0])
+
+ default:
+ ctx.Errorf("multiple primary builder modules present:")
+ for _, primaryBuilder := range primaryBuilders {
+ ctx.ModuleErrorf(primaryBuilder, "<-- module %s",
+ ctx.ModuleName(primaryBuilder))
+ }
+ return
+ }
+
+ primaryBuilderFile := filepath.Join(binDir, primaryBuilderName)
+
+ // Get the filename of the top-level Blueprints file to pass to minibp.
+ // This comes stored in a global variable that's set by Main.
+ topLevelBlueprints := filepath.Join("$SrcDir",
+ filepath.Base(topLevelBlueprintsFile))
+
+ tmpNinjaFile := filepath.Join("bootstrap", "build.ninja.in")
+ tmpNinjaDepFile := tmpNinjaFile + ".d"
+
+ if generatingBootstrapper(ctx.Config()) {
+ // We're generating a bootstrapper Ninja file, so we need to set things
+ // up to rebuild the build.ninja file using the primary builder.
+
+ // We generate the depfile here that includes the dependencies for all
+ // the Blueprints files that contribute to generating the big build
+ // manifest (build.ninja file). This depfile will be used by the non-
+ // bootstrap build manifest to determine whether it should trigger a re-
+ // bootstrap. Because the re-bootstrap rule's output is "build.ninja"
+ // we need to force the depfile to have that as its "make target"
+ // (recall that depfiles use a subset of the Makefile syntax).
+ bigbp := ctx.Rule("bigbp",
+ blueprint.RuleParams{
+ Command: fmt.Sprintf("%s %s -d %s -o $out $in",
+ primaryBuilderFile, primaryBuilderExtraFlags,
+ tmpNinjaDepFile),
+ Description: fmt.Sprintf("%s $out", primaryBuilderName),
+ Depfile: tmpNinjaDepFile,
+ })
+
+ ctx.Build(blueprint.BuildParams{
+ Rule: bigbp,
+ Outputs: []string{tmpNinjaFile},
+ Inputs: []string{topLevelBlueprints},
+ Implicits: []string{primaryBuilderFile},
+ })
+
+ // When the current build.ninja file is a bootstrapper, we always want
+ // to have it replace itself with a non-bootstrapper build.ninja. To
+ // accomplish that we depend on a file that should never exist and
+ // "build" it using Ninja's built-in phony rule.
+ //
+ // We also need to add an implicit dependency on the minibp binary so
+ // that it actually gets built. Nothing in the bootstrap build.ninja
+ // file actually requires minibp, but the non-bootstrap build.ninja
+ // requires that it have been built during the bootstrapping.
+ notAFile := filepath.Join("bootstrap", "notAFile")
+ ctx.Build(blueprint.BuildParams{
+ Rule: blueprint.Phony,
+ Outputs: []string{notAFile},
+ })
+
+ ctx.Build(blueprint.BuildParams{
+ Rule: bootstrap,
+ Outputs: []string{"build.ninja"},
+ Inputs: []string{tmpNinjaFile},
+ Implicits: []string{"$Bootstrap", notAFile, minibpFile},
+ })
+
+ // Because the non-bootstrap build.ninja file manually re-invokes Ninja,
+ // its builddir must be different than that of the bootstrap build.ninja
+ // file. Otherwise we occasionally get "warning: bad deps log signature
+ // or version; starting over" messages from Ninja, presumably because
+ // two Ninja processes try to write to the same log concurrently.
+ ctx.SetBuildDir("bootstrap")
+ } else {
+ // We're generating a non-bootstrapper Ninja file, so we need to set it
+ // up to depend on the bootstrapper Ninja file. The build.ninja target
+ // also has an implicit dependency on the primary builder, which will
+ // have a phony dependency on all its sources. This will cause any
+ // changes to the primary builder's sources to trigger a re-bootstrap
+ // operation, which will rebuild the primary builder.
+ //
+ // On top of that we need to use the depfile generated by the bigbp
+ // rule. We do this by depending on that file and then setting up a
+ // phony rule to generate it that uses the depfile.
+ ctx.Build(blueprint.BuildParams{
+ Rule: rebootstrap,
+ Outputs: []string{"build.ninja"},
+ Inputs: []string{"$BootstrapManifest"},
+ Implicits: []string{"$Bootstrap", primaryBuilderFile, tmpNinjaFile},
+ })
+
+ ctx.Build(blueprint.BuildParams{
+ Rule: phony,
+ Outputs: []string{tmpNinjaFile},
+ Inputs: []string{topLevelBlueprints},
+ Args: map[string]string{
+ "depfile": tmpNinjaDepFile,
+ },
+ })
+
+ // Rebuild the bootstrap Ninja file using minibp, passing it all the
+ // Blueprint files that define a bootstrap_* module.
+ ctx.Build(blueprint.BuildParams{
+ Rule: minibp,
+ Outputs: []string{"$BootstrapManifest"},
+ Inputs: []string{topLevelBlueprints},
+ Implicits: []string{minibpFile},
+ })
+ }
+}
+
+// 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 {
+ return filepath.Join("bootstrap", ctx.ModuleName(), "pkg")
+}
+
+// srcDir returns the path of the directory that all source file paths are
+// specified relative to.
+func srcDir(ctx blueprint.ModuleContext) string {
+ return filepath.Join("$SrcDir", ctx.ModuleDir())
+}
+
+// objDir returns the module-specific object directory path.
+func objDir(ctx blueprint.ModuleContext) string {
+ return filepath.Join("bootstrap", ctx.ModuleName(), "obj")
+}
+
+// PrefixPaths returns a list of paths consisting of prefix joined with each
+// element of paths. The resulting paths are "clean" in the filepath.Clean
+// sense.
+//
+// TODO: This should probably go in a utility package.
+func PrefixPaths(paths []string, prefix string) []string {
+ result := make([]string, len(paths))
+ for i, path := range paths {
+ result[i] = filepath.Clean(filepath.Join(prefix, path))
+ }
+ return result
+}
diff --git a/blueprint/bootstrap/command.go b/blueprint/bootstrap/command.go
new file mode 100644
index 0000000..9a0c32e
--- /dev/null
+++ b/blueprint/bootstrap/command.go
@@ -0,0 +1,147 @@
+package bootstrap
+
+import (
+ "blueprint"
+ "bytes"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "strings"
+)
+
+var outFile string
+var depFile string
+var depTarget string
+
+// topLevelBlueprintsFile is set by Main as a way to pass this information on to
+// the bootstrap build manifest generators. This information was not passed via
+// the Config object so as to allow the caller of Main to use whatever Config
+// object it wants.
+var topLevelBlueprintsFile string
+
+func init() {
+ flag.StringVar(&outFile, "o", "build.ninja.in", "the Ninja file to output")
+ flag.StringVar(&depFile, "d", "", "the dependency file to output")
+ flag.StringVar(&depTarget, "t", "", "the target name for the dependency "+
+ "file")
+}
+
+func Main(ctx *blueprint.Context, config blueprint.Config) {
+ if !flag.Parsed() {
+ flag.Parse()
+ }
+
+ ctx.RegisterModuleType("bootstrap_go_package", goPackageModule)
+ ctx.RegisterModuleType("bootstrap_go_binary", goBinaryModule)
+ ctx.RegisterSingleton("bootstrap", newSingleton())
+
+ if flag.NArg() != 1 {
+ fatalf("no Blueprints file specified")
+ }
+
+ topLevelBlueprintsFile = flag.Arg(0)
+
+ deps, errs := ctx.ParseBlueprintsFiles(topLevelBlueprintsFile)
+ if len(errs) > 0 {
+ fatalErrors(errs)
+ }
+
+ errs = ctx.PrepareBuildActions(config)
+ if len(errs) > 0 {
+ fatalErrors(errs)
+ }
+
+ buf := bytes.NewBuffer(nil)
+ err := ctx.WriteBuildFile(buf)
+ if err != nil {
+ fatalf("error generating Ninja file contents: %s", err)
+ }
+
+ err = writeFileIfChanged(outFile, buf.Bytes(), 0666)
+ if err != nil {
+ fatalf("error writing %s: %s", outFile, err)
+ }
+
+ if depFile != "" {
+ f, err := os.Create(depFile)
+ if err != nil {
+ fatalf("error creating depfile: %s", err)
+ }
+
+ target := depTarget
+ if target == "" {
+ target = outFile
+ }
+
+ _, err = fmt.Fprintf(f, "%s: \\\n %s\n", target,
+ strings.Join(deps, " \\\n "))
+ if err != nil {
+ fatalf("error writing depfile: %s", err)
+ }
+
+ f.Close()
+ }
+
+ os.Exit(0)
+}
+
+func fatalf(format string, args ...interface{}) {
+ fmt.Fprintf(os.Stderr, format, args...)
+ os.Exit(1)
+}
+
+func fatalErrors(errs []error) {
+ for _, err := range errs {
+ switch err.(type) {
+ case *blueprint.Error:
+ _, _ = fmt.Fprintf(os.Stderr, "%s\n", err.Error())
+ default:
+ _, _ = fmt.Fprintf(os.Stderr, "internal error: %s\n", err)
+ }
+ }
+ os.Exit(1)
+}
+
+func writeFileIfChanged(filename string, data []byte, perm os.FileMode) error {
+ var isChanged bool
+
+ info, err := os.Stat(filename)
+ if err != nil {
+ if os.IsNotExist(err) {
+ // The file does not exist yet.
+ isChanged = true
+ } else {
+ return err
+ }
+ } else {
+ if info.Size() != int64(len(data)) {
+ isChanged = true
+ } else {
+ oldData, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return err
+ }
+
+ if len(oldData) != len(data) {
+ isChanged = true
+ } else {
+ for i := range data {
+ if oldData[i] != data[i] {
+ isChanged = true
+ break
+ }
+ }
+ }
+ }
+ }
+
+ if isChanged {
+ err = ioutil.WriteFile(filename, data, perm)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/blueprint/bootstrap/config.go b/blueprint/bootstrap/config.go
new file mode 100644
index 0000000..2f41499
--- /dev/null
+++ b/blueprint/bootstrap/config.go
@@ -0,0 +1,29 @@
+package bootstrap
+
+import (
+ "blueprint"
+)
+
+var (
+ // These variables are the only only configuration needed by the boostrap
+ // modules. They are always set to the variable name enclosed in "@@" so
+ // that their values can be easily replaced in the generated Ninja file.
+ SrcDir = blueprint.StaticVariable("SrcDir", "@@SrcDir@@")
+ GoRoot = blueprint.StaticVariable("GoRoot", "@@GoRoot@@")
+ GoOS = blueprint.StaticVariable("GoOS", "@@GoOS@@")
+ GoArch = blueprint.StaticVariable("GoArch", "@@GoArch@@")
+ GoChar = blueprint.StaticVariable("GoChar", "@@GoChar@@")
+ Bootstrap = blueprint.StaticVariable("Bootstrap", "@@Bootstrap@@")
+ BootstrapManifest = blueprint.StaticVariable("BootstrapManifest",
+ "@@BootstrapManifest@@")
+
+ goToolDir = blueprint.StaticVariable("goToolDir",
+ "$GoRoot/pkg/tool/${GoOS}_$GoArch")
+)
+
+type Config interface {
+ // GeneratingBootstrapper should return true if this build invocation is
+ // creating a build.ninja.in file to be used in a build bootstrapping
+ // sequence.
+ GeneratingBootstrapper() bool
+}
diff --git a/blueprint/bootstrap/doc.go b/blueprint/bootstrap/doc.go
new file mode 100644
index 0000000..bb7771c
--- /dev/null
+++ b/blueprint/bootstrap/doc.go
@@ -0,0 +1,29 @@
+/*
+
+The Blueprint bootstrapping mechanism is intended to enable building a source
+tree using a Blueprint-based build system that is embedded (as source) in that
+source tree. The only prerequisites for performing such a build are:
+ 1. A Ninja binary
+ 2. A script interpreter (e.g. Bash or Python)
+ 3. A Go toolchain
+
+The bootstrapping process is intended to be customized for the source tree in
+which it is embedded. As an initial example, we'll examine the bootstrapping
+system used to build the core Blueprint packages. Bootstrapping the core
+Blueprint packages involves two files:
+
+ bootstrap.bash: When this script is run it initializes the current
+ working directory as a build output directory. It does
+ this by first automatically determining the root source
+ directory and Go build environment. It then uses those
+ values to do a simple string replacement over the
+ build.ninja.in file contents, and places the result into
+ the current working directory.
+
+ build.ninja.in: This file is generated by passing all the Blueprint
+
+ files needed to construct the primary builder
+
+
+*/
+package bootstrap
diff --git a/blueprint/bootstrap/minibp/main.go b/blueprint/bootstrap/minibp/main.go
new file mode 100644
index 0000000..c19dd4f
--- /dev/null
+++ b/blueprint/bootstrap/minibp/main.go
@@ -0,0 +1,32 @@
+package main
+
+import (
+ "blueprint"
+ "blueprint/bootstrap"
+ "flag"
+)
+
+var runAsPrimaryBuilder bool
+
+func init() {
+ flag.BoolVar(&runAsPrimaryBuilder, "p", false, "run as a primary builder")
+}
+
+type Config bool
+
+func (c Config) GeneratingBootstrapper() bool {
+ return bool(c)
+}
+
+func main() {
+ flag.Parse()
+
+ ctx := blueprint.NewContext()
+ if !runAsPrimaryBuilder {
+ ctx.SetIgnoreUnknownModuleTypes(true)
+ }
+
+ config := Config(!runAsPrimaryBuilder)
+
+ bootstrap.Main(ctx, config)
+}
diff --git a/blueprint/context.go b/blueprint/context.go
new file mode 100644
index 0000000..5af899f
--- /dev/null
+++ b/blueprint/context.go
@@ -0,0 +1,1363 @@
+package blueprint
+
+import (
+ "blueprint/parser"
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "reflect"
+ "sort"
+ "strings"
+ "text/scanner"
+ "text/template"
+)
+
+var ErrBuildActionsNotReady = errors.New("build actions are not ready")
+
+const maxErrors = 10
+
+type Context struct {
+ // set at instantiation
+ moduleTypes map[string]ModuleType
+ modules map[string]Module
+ moduleInfo map[Module]*moduleInfo
+ singletonInfo map[string]*singletonInfo
+
+ dependenciesReady bool // set to true on a successful ResolveDependencies
+ buildActionsReady bool // set to true on a successful PrepareBuildActions
+
+ // set by SetIgnoreUnknownModuleTypes
+ ignoreUnknownModuleTypes bool
+
+ // set during PrepareBuildActions
+ pkgNames map[*pkg]string
+ globalVariables map[Variable]*ninjaString
+ globalPools map[Pool]*poolDef
+ globalRules map[Rule]*ruleDef
+
+ // set during PrepareBuildActions
+ buildDir *ninjaString // The builddir special Ninja variable
+ requiredNinjaMajor int // For the ninja_required_version variable
+ requiredNinjaMinor int // For the ninja_required_version variable
+ requiredNinjaMicro int // For the ninja_required_version variable
+}
+
+// A Config contains build configuration information that can affect the
+// contents of the Ninja build file is that will be generated. The specific
+// representation of this configuration information is not defined here.
+type Config interface{}
+
+type Error struct {
+ Err error
+ Pos scanner.Position
+}
+
+type localBuildActions struct {
+ variables []*localVariable
+ rules []*localRule
+ buildDefs []*buildDef
+}
+
+type moduleInfo struct {
+ // set during Parse
+ typeName string
+ typ ModuleType
+ relBlueprintFile string
+ pos scanner.Position
+ propertyPos map[string]scanner.Position
+ properties struct {
+ Name string
+ Deps []string
+ }
+
+ // set during ResolveDependencies
+ directDeps []Module
+
+ // set during PrepareBuildActions
+ actionDefs localBuildActions
+}
+
+type singletonInfo struct {
+ // set during RegisterSingleton
+ singleton Singleton
+
+ // set during PrepareBuildActions
+ actionDefs localBuildActions
+}
+
+func (e *Error) Error() string {
+
+ return fmt.Sprintf("%s: %s", e.Pos, e.Err)
+}
+
+func NewContext() *Context {
+ return &Context{
+ moduleTypes: make(map[string]ModuleType),
+ modules: make(map[string]Module),
+ moduleInfo: make(map[Module]*moduleInfo),
+ singletonInfo: make(map[string]*singletonInfo),
+ }
+}
+
+func (c *Context) RegisterModuleType(name string, typ ModuleType) {
+ if _, present := c.moduleTypes[name]; present {
+ panic(errors.New("module type name is already registered"))
+ }
+ c.moduleTypes[name] = typ
+}
+
+func (c *Context) RegisterSingleton(name string, singleton Singleton) {
+ if _, present := c.singletonInfo[name]; present {
+ panic(errors.New("singleton name is already registered"))
+ }
+ if singletonPkgPath(singleton) == "" {
+ panic(errors.New("singleton types must be a named type"))
+ }
+ c.singletonInfo[name] = &singletonInfo{
+ singleton: singleton,
+ }
+}
+
+func singletonPkgPath(singleton Singleton) string {
+ typ := reflect.TypeOf(singleton)
+ for typ.Kind() == reflect.Ptr {
+ typ = typ.Elem()
+ }
+ return typ.PkgPath()
+}
+
+func singletonTypeName(singleton Singleton) string {
+ typ := reflect.TypeOf(singleton)
+ for typ.Kind() == reflect.Ptr {
+ typ = typ.Elem()
+ }
+ return typ.PkgPath() + "." + typ.Name()
+}
+
+func (c *Context) SetIgnoreUnknownModuleTypes(ignoreUnknownModuleTypes bool) {
+ c.ignoreUnknownModuleTypes = ignoreUnknownModuleTypes
+}
+
+func (c *Context) Parse(rootDir, filename string, r io.Reader) (subdirs []string,
+ errs []error) {
+
+ c.dependenciesReady = false
+
+ relBlueprintFile, err := filepath.Rel(rootDir, filename)
+ if err != nil {
+ return nil, []error{err}
+ }
+
+ defs, errs := parser.Parse(filename, r)
+ if len(errs) > 0 {
+ for i, err := range errs {
+ if parseErr, ok := err.(*parser.ParseError); ok {
+ err = &Error{
+ Err: parseErr.Err,
+ Pos: parseErr.Pos,
+ }
+ errs[i] = err
+ }
+ }
+
+ // If there were any parse errors don't bother trying to interpret the
+ // result.
+ return nil, errs
+ }
+
+ for _, def := range defs {
+ var newErrs []error
+ switch def := def.(type) {
+ case *parser.Module:
+ newErrs = c.processModuleDef(def, relBlueprintFile)
+
+ case *parser.Assignment:
+ var newSubdirs []string
+ newSubdirs, newErrs = c.processAssignment(def)
+ if newSubdirs != nil {
+ subdirs = newSubdirs
+ }
+
+ default:
+ panic("unknown definition type")
+ }
+
+ if len(newErrs) > 0 {
+ errs = append(errs, newErrs...)
+ if len(errs) > maxErrors {
+ break
+ }
+ }
+ }
+
+ return subdirs, errs
+}
+
+func (c *Context) ParseBlueprintsFiles(rootFile string) (deps []string,
+ errs []error) {
+
+ rootDir := filepath.Dir(rootFile)
+
+ depsSet := map[string]bool{rootFile: true}
+ blueprints := []string{rootFile}
+
+ var file *os.File
+ defer func() {
+ if file != nil {
+ file.Close()
+ }
+ }()
+
+ var err error
+
+ for i := 0; i < len(blueprints); i++ {
+ if len(errs) > maxErrors {
+ return
+ }
+
+ filename := blueprints[i]
+ dir := filepath.Dir(filename)
+
+ file, err = os.Open(filename)
+ if err != nil {
+ errs = append(errs, &Error{Err: err})
+ continue
+ }
+
+ subdirs, newErrs := c.Parse(rootDir, filename, file)
+ if len(newErrs) > 0 {
+ errs = append(errs, newErrs...)
+ continue
+ }
+
+ err = file.Close()
+ if err != nil {
+ errs = append(errs, &Error{Err: err})
+ continue
+ }
+
+ // Add the subdirs to the list of directories to parse Blueprint files
+ // from.
+ for _, subdir := range subdirs {
+ subdir = filepath.Join(dir, subdir)
+ dirPart, filePart := filepath.Split(subdir)
+ dirPart = filepath.Clean(dirPart)
+
+ if filePart == "*" {
+ foundSubdirs, err := listSubdirs(dirPart)
+ if err != nil {
+ errs = append(errs, &Error{Err: err})
+ continue
+ }
+
+ for _, foundSubdir := range foundSubdirs {
+ subBlueprints := filepath.Join(dirPart, foundSubdir,
+ "Blueprints")
+
+ _, err := os.Stat(subBlueprints)
+ if os.IsNotExist(err) {
+ // There is no Blueprints file in this subdirectory. We
+ // need to add the directory to the list of dependencies
+ // so that if someone adds a Blueprints file in the
+ // future we'll pick it up.
+ depsSet[filepath.Dir(subBlueprints)] = true
+ } else if !depsSet[subBlueprints] {
+ // We haven't seen this Blueprints file before, so add
+ // it to our list.
+ depsSet[subBlueprints] = true
+ blueprints = append(blueprints, subBlueprints)
+ }
+ }
+
+ // We now depend on the directory itself because if any new
+ // subdirectories get added or removed we need to rebuild the
+ // Ninja manifest.
+ depsSet[dirPart] = true
+ } else {
+ subBlueprints := filepath.Join(subdir, "Blueprints")
+ if !depsSet[subBlueprints] {
+ depsSet[subBlueprints] = true
+ blueprints = append(blueprints, subBlueprints)
+ }
+ }
+ }
+ }
+
+ for dep := range depsSet {
+ deps = append(deps, dep)
+ }
+
+ return
+}
+
+func listSubdirs(dir string) ([]string, error) {
+ d, err := os.Open(dir)
+ if err != nil {
+ return nil, err
+ }
+ defer d.Close()
+
+ infos, err := d.Readdir(-1)
+ if err != nil {
+ return nil, err
+ }
+
+ var subdirs []string
+ for _, info := range infos {
+ if info.IsDir() {
+ subdirs = append(subdirs, info.Name())
+ }
+ }
+
+ return subdirs, nil
+}
+
+func (c *Context) processAssignment(
+ assignment *parser.Assignment) (subdirs []string, errs []error) {
+
+ if assignment.Name == "subdirs" {
+ switch assignment.Value.Type {
+ case parser.List:
+ subdirs = make([]string, 0, len(assignment.Value.ListValue))
+
+ for _, value := range assignment.Value.ListValue {
+ if value.Type != parser.String {
+ // The parser should not produce this.
+ panic("non-string value found in list")
+ }
+
+ dirPart, filePart := filepath.Split(value.StringValue)
+ if (filePart != "*" && strings.ContainsRune(filePart, '*')) ||
+ strings.ContainsRune(dirPart, '*') {
+
+ errs = append(errs, &Error{
+ Err: fmt.Errorf("subdirs may only wildcard whole " +
+ "directories"),
+ Pos: value.Pos,
+ })
+
+ continue
+ }
+
+ subdirs = append(subdirs, value.StringValue)
+ }
+
+ if len(errs) > 0 {
+ subdirs = nil
+ }
+
+ return
+
+ case parser.Bool, parser.String:
+ errs = []error{
+ &Error{
+ Err: fmt.Errorf("subdirs must be a list of strings"),
+ Pos: assignment.Pos,
+ },
+ }
+
+ return
+
+ default:
+ panic(fmt.Errorf("unknown value type: %d", assignment.Value.Type))
+ }
+ }
+
+ return nil, []error{
+ &Error{
+ Err: fmt.Errorf("only 'subdirs' assignment is supported"),
+ Pos: assignment.Pos,
+ },
+ }
+}
+
+func (c *Context) processModuleDef(moduleDef *parser.Module,
+ relBlueprintFile string) []error {
+
+ typeName := moduleDef.Type
+ typ, ok := c.moduleTypes[typeName]
+ if !ok {
+ if c.ignoreUnknownModuleTypes {
+ return nil
+ }
+
+ err := fmt.Errorf("unrecognized module type %q", typeName)
+ return []error{err}
+ }
+
+ module, properties := typ.new()
+ info := &moduleInfo{
+ typeName: typeName,
+ typ: typ,
+ relBlueprintFile: relBlueprintFile,
+ }
+
+ errs := unpackProperties(moduleDef.Properties, &info.properties,
+ properties)
+ if len(errs) > 0 {
+ return errs
+ }
+
+ info.pos = moduleDef.Pos
+ info.propertyPos = make(map[string]scanner.Position)
+ for _, propertyDef := range moduleDef.Properties {
+ info.propertyPos[propertyDef.Name] = propertyDef.Pos
+ }
+
+ name := info.properties.Name
+ err := validateNinjaName(name)
+ if err != nil {
+ return []error{
+ &Error{
+ Err: fmt.Errorf("invalid module name %q: %s", err),
+ Pos: info.propertyPos["name"],
+ },
+ }
+ }
+
+ if first, present := c.modules[name]; present {
+ errs = append(errs, &Error{
+ Err: fmt.Errorf("module %q already defined", name),
+ Pos: moduleDef.Pos,
+ })
+ errs = append(errs, &Error{
+ Err: fmt.Errorf("<-- previous definition here"),
+ Pos: c.moduleInfo[first].pos,
+ })
+ if len(errs) >= maxErrors {
+ return errs
+ }
+ }
+
+ c.modules[name] = module
+ c.moduleInfo[module] = info
+
+ return nil
+}
+
+func (c *Context) ResolveDependencies() []error {
+ errs := c.resolveDependencies()
+ if len(errs) > 0 {
+ return errs
+ }
+
+ errs = c.checkForDependencyCycles()
+ if len(errs) > 0 {
+ return errs
+ }
+
+ c.dependenciesReady = true
+ return nil
+}
+
+// resolveDependencies populates the moduleInfo.directDeps list for every
+// module. In doing so it checks for missing dependencies and self-dependant
+// modules.
+func (c *Context) resolveDependencies() (errs []error) {
+ for _, info := range c.moduleInfo {
+ depNames := info.properties.Deps
+ info.directDeps = make([]Module, 0, len(depNames))
+ depsPos := info.propertyPos["deps"]
+
+ for _, depName := range depNames {
+ if depName == info.properties.Name {
+ errs = append(errs, &Error{
+ Err: fmt.Errorf("%q depends on itself", depName),
+ Pos: depsPos,
+ })
+ continue
+ }
+
+ dep, ok := c.modules[depName]
+ if !ok {
+ errs = append(errs, &Error{
+ Err: fmt.Errorf("%q depends on undefined module %q",
+ info.properties.Name, depName),
+ Pos: depsPos,
+ })
+ continue
+ }
+
+ info.directDeps = append(info.directDeps, dep)
+ }
+ }
+
+ return
+}
+
+// checkForDependencyCycles recursively walks the module dependency graph and
+// reports errors when it encounters dependency cycles. This should only be
+// called after resolveDependencies.
+func (c *Context) checkForDependencyCycles() (errs []error) {
+ visited := make(map[Module]bool) // modules that were already checked
+ checking := make(map[Module]bool) // modules actively being checked
+
+ var check func(m Module) []Module
+
+ check = func(m Module) []Module {
+ info := c.moduleInfo[m]
+
+ visited[m] = true
+ checking[m] = true
+ defer delete(checking, m)
+
+ for _, dep := range info.directDeps {
+ if checking[dep] {
+ // This is a cycle.
+ return []Module{dep, m}
+ }
+
+ if !visited[dep] {
+ cycle := check(dep)
+ if cycle != nil {
+ if cycle[0] == m {
+ // We are the "start" of the cycle, so we're responsible
+ // for generating the errors. The cycle list is in
+ // reverse order because all the 'check' calls append
+ // their own module to the list.
+ errs = append(errs, &Error{
+ Err: fmt.Errorf("encountered dependency cycle:"),
+ Pos: info.pos,
+ })
+
+ // Iterate backwards through the cycle list.
+ curInfo := info
+ for i := len(cycle) - 1; i >= 0; i-- {
+ nextInfo := c.moduleInfo[cycle[i]]
+ errs = append(errs, &Error{
+ Err: fmt.Errorf(" %q depends on %q",
+ curInfo.properties.Name,
+ nextInfo.properties.Name),
+ Pos: curInfo.propertyPos["deps"],
+ })
+ curInfo = nextInfo
+ }
+
+ // We can continue processing this module's children to
+ // find more cycles. Since all the modules that were
+ // part of the found cycle were marked as visited we
+ // won't run into that cycle again.
+ } else {
+ // We're not the "start" of the cycle, so we just append
+ // our module to the list and return it.
+ return append(cycle, m)
+ }
+ }
+ }
+ }
+
+ return nil
+ }
+
+ for _, module := range c.modules {
+ if !visited[module] {
+ cycle := check(module)
+ if cycle != nil {
+ panic("inconceivable!")
+ }
+ }
+ }
+
+ return
+}
+
+func (c *Context) PrepareBuildActions(config Config) []error {
+ c.buildActionsReady = false
+
+ if !c.dependenciesReady {
+ errs := c.ResolveDependencies()
+ if len(errs) > 0 {
+ return errs
+ }
+ }
+
+ liveGlobals := newLiveTracker(config)
+
+ c.initSpecialVariables()
+
+ errs := c.generateModuleBuildActions(config, liveGlobals)
+ if len(errs) > 0 {
+ return errs
+ }
+
+ errs = c.generateSingletonBuildActions(config, liveGlobals)
+ if len(errs) > 0 {
+ return errs
+ }
+
+ if c.buildDir != nil {
+ liveGlobals.addNinjaStringDeps(c.buildDir)
+ }
+
+ pkgNames := c.makeUniquePackageNames(liveGlobals)
+
+ // This will panic if it finds a problem since it's a programming error.
+ c.checkForVariableReferenceCycles(liveGlobals.variables, pkgNames)
+
+ c.pkgNames = pkgNames
+ c.globalVariables = liveGlobals.variables
+ c.globalPools = liveGlobals.pools
+ c.globalRules = liveGlobals.rules
+
+ c.buildActionsReady = true
+
+ return nil
+}
+
+func (c *Context) initSpecialVariables() {
+ c.buildDir = nil
+ c.requiredNinjaMajor = 1
+ c.requiredNinjaMinor = 1
+ c.requiredNinjaMicro = 0
+}
+
+func (c *Context) generateModuleBuildActions(config Config,
+ liveGlobals *liveTracker) []error {
+
+ visited := make(map[Module]bool)
+
+ var errs []error
+
+ var walk func(module Module)
+ walk = func(module Module) {
+ visited[module] = true
+
+ info := c.moduleInfo[module]
+ for _, dep := range info.directDeps {
+ if !visited[dep] {
+ walk(dep)
+ }
+ }
+
+ mctx := &moduleContext{
+ context: c,
+ config: config,
+ module: module,
+ scope: newLocalScope(info.typ.pkg().scope,
+ moduleNamespacePrefix(info.properties.Name)),
+ info: info,
+ }
+
+ module.GenerateBuildActions(mctx)
+
+ if len(mctx.errs) > 0 {
+ errs = append(errs, mctx.errs...)
+ return
+ }
+
+ newErrs := c.processLocalBuildActions(&info.actionDefs,
+ &mctx.actionDefs, liveGlobals)
+ errs = append(errs, newErrs...)
+ }
+
+ for _, module := range c.modules {
+ if !visited[module] {
+ walk(module)
+ }
+ }
+
+ return errs
+}
+
+func (c *Context) generateSingletonBuildActions(config Config,
+ liveGlobals *liveTracker) []error {
+
+ var errs []error
+ for name, info := range c.singletonInfo {
+ // If the package to which the singleton type belongs has not defined
+ // any Ninja globals and has not called Import() then we won't have an
+ // entry for it in the pkgs map. If that's the case then the
+ // singleton's scope's parent should just be nil.
+ var singletonScope *scope
+ if pkg := pkgs[singletonPkgPath(info.singleton)]; pkg != nil {
+ singletonScope = pkg.scope
+ }
+
+ sctx := &singletonContext{
+ context: c,
+ config: config,
+ scope: newLocalScope(singletonScope,
+ singletonNamespacePrefix(name)),
+ }
+
+ info.singleton.GenerateBuildActions(sctx)
+
+ if len(sctx.errs) > 0 {
+ errs = append(errs, sctx.errs...)
+ if len(errs) > maxErrors {
+ break
+ }
+ continue
+ }
+
+ newErrs := c.processLocalBuildActions(&info.actionDefs,
+ &sctx.actionDefs, liveGlobals)
+ errs = append(errs, newErrs...)
+ if len(errs) > maxErrors {
+ break
+ }
+ }
+
+ return errs
+}
+
+func (c *Context) processLocalBuildActions(out, in *localBuildActions,
+ liveGlobals *liveTracker) []error {
+
+ var errs []error
+
+ // First we go through and add everything referenced by the module's
+ // buildDefs to the live globals set. This will end up adding the live
+ // locals to the set as well, but we'll take them out after.
+ for _, def := range in.buildDefs {
+ err := liveGlobals.AddBuildDefDeps(def)
+ if err != nil {
+ errs = append(errs, err)
+ }
+ }
+
+ if len(errs) > 0 {
+ return errs
+ }
+
+ out.buildDefs = in.buildDefs
+
+ // We use the now-incorrect set of live "globals" to determine which local
+ // definitions are live. As we go through copying those live locals to the
+ // moduleInfo we remove them from the live globals set.
+ out.variables = nil
+ for _, v := range in.variables {
+ _, isLive := liveGlobals.variables[v]
+ if isLive {
+ out.variables = append(out.variables, v)
+ delete(liveGlobals.variables, v)
+ }
+ }
+
+ out.rules = nil
+ for _, r := range in.rules {
+ _, isLive := liveGlobals.rules[r]
+ if isLive {
+ out.rules = append(out.rules, r)
+ delete(liveGlobals.rules, r)
+ }
+ }
+
+ return nil
+}
+
+func (c *Context) visitDepsDepthFirst(module Module, visit func(Module)) {
+ visited := make(map[Module]bool)
+
+ var walk func(m Module)
+ walk = func(m Module) {
+ info := c.moduleInfo[m]
+ visited[m] = true
+ for _, dep := range info.directDeps {
+ if !visited[dep] {
+ walk(dep)
+ }
+ }
+ visit(m)
+ }
+
+ info := c.moduleInfo[module]
+ for _, dep := range info.directDeps {
+ if !visited[dep] {
+ walk(dep)
+ }
+ }
+}
+
+func (c *Context) visitDepsDepthFirstIf(module Module, pred func(Module) bool,
+ visit func(Module)) {
+
+ visited := make(map[Module]bool)
+
+ var walk func(m Module)
+ walk = func(m Module) {
+ info := c.moduleInfo[m]
+ visited[m] = true
+ if pred(m) {
+ for _, dep := range info.directDeps {
+ if !visited[dep] {
+ walk(dep)
+ }
+ }
+ visit(m)
+ }
+ }
+
+ info := c.moduleInfo[module]
+ for _, dep := range info.directDeps {
+ if !visited[dep] {
+ walk(dep)
+ }
+ }
+}
+
+func (c *Context) visitAllModules(visit func(Module)) {
+ for _, module := range c.modules {
+ visit(module)
+ }
+}
+
+func (c *Context) visitAllModulesIf(pred func(Module) bool,
+ visit func(Module)) {
+
+ for _, module := range c.modules {
+ if pred(module) {
+ visit(module)
+ }
+ }
+}
+
+func (c *Context) requireNinjaVersion(major, minor, micro int) {
+ if major != 1 {
+ panic("ninja version with major version != 1 not supported")
+ }
+ if c.requiredNinjaMinor < minor {
+ c.requiredNinjaMinor = minor
+ c.requiredNinjaMicro = micro
+ }
+ if c.requiredNinjaMinor == minor && c.requiredNinjaMicro < micro {
+ c.requiredNinjaMicro = micro
+ }
+}
+
+func (c *Context) setBuildDir(value *ninjaString) {
+ if c.buildDir != nil {
+ panic("buildDir set multiple times")
+ }
+ c.buildDir = value
+}
+
+func (c *Context) makeUniquePackageNames(
+ liveGlobals *liveTracker) map[*pkg]string {
+
+ pkgs := make(map[string]*pkg)
+ pkgNames := make(map[*pkg]string)
+ longPkgNames := make(map[*pkg]bool)
+
+ processPackage := func(pkg *pkg) {
+ if pkg == nil {
+ // This is a built-in rule and has no package.
+ return
+ }
+ if _, ok := pkgNames[pkg]; ok {
+ // We've already processed this package.
+ return
+ }
+
+ otherPkg, present := pkgs[pkg.shortName]
+ if present {
+ // Short name collision. Both this package and the one that's
+ // already there need to use their full names. We leave the short
+ // name in pkgNames for now so future collisions still get caught.
+ longPkgNames[pkg] = true
+ longPkgNames[otherPkg] = true
+ } else {
+ // No collision so far. Tentatively set the package's name to be
+ // its short name.
+ pkgNames[pkg] = pkg.shortName
+ }
+ }
+
+ // We try to give all packages their short name, but when we get collisions
+ // we need to use the full unique package name.
+ for v, _ := range liveGlobals.variables {
+ processPackage(v.pkg())
+ }
+ for p, _ := range liveGlobals.pools {
+ processPackage(p.pkg())
+ }
+ for r, _ := range liveGlobals.rules {
+ processPackage(r.pkg())
+ }
+
+ // Add the packages that had collisions using their full unique names. This
+ // will overwrite any short names that were added in the previous step.
+ for pkg := range longPkgNames {
+ pkgNames[pkg] = pkg.fullName
+ }
+
+ return pkgNames
+}
+
+func (c *Context) checkForVariableReferenceCycles(
+ variables map[Variable]*ninjaString, pkgNames map[*pkg]string) {
+
+ visited := make(map[Variable]bool) // variables that were already checked
+ checking := make(map[Variable]bool) // variables actively being checked
+
+ var check func(v Variable) []Variable
+
+ check = func(v Variable) []Variable {
+ visited[v] = true
+ checking[v] = true
+ defer delete(checking, v)
+
+ value := variables[v]
+ for _, dep := range value.variables {
+ if checking[dep] {
+ // This is a cycle.
+ return []Variable{dep, v}
+ }
+
+ if !visited[dep] {
+ cycle := check(dep)
+ if cycle != nil {
+ if cycle[0] == v {
+ // We are the "start" of the cycle, so we're responsible
+ // for generating the errors. The cycle list is in
+ // reverse order because all the 'check' calls append
+ // their own module to the list.
+ msgs := []string{"detected variable reference cycle:"}
+
+ // Iterate backwards through the cycle list.
+ curName := v.fullName(pkgNames)
+ curValue := value.Value(pkgNames)
+ for i := len(cycle) - 1; i >= 0; i-- {
+ next := cycle[i]
+ nextName := next.fullName(pkgNames)
+ nextValue := variables[next].Value(pkgNames)
+
+ msgs = append(msgs, fmt.Sprintf(
+ " %q depends on %q", curName, nextName))
+ msgs = append(msgs, fmt.Sprintf(
+ " [%s = %s]", curName, curValue))
+
+ curName = nextName
+ curValue = nextValue
+ }
+
+ // Variable reference cycles are a programming error,
+ // not the fault of the Blueprint file authors.
+ panic(strings.Join(msgs, "\n"))
+ } else {
+ // We're not the "start" of the cycle, so we just append
+ // our module to the list and return it.
+ return append(cycle, v)
+ }
+ }
+ }
+ }
+
+ return nil
+ }
+
+ for v := range variables {
+ if !visited[v] {
+ cycle := check(v)
+ if cycle != nil {
+ panic("inconceivable!")
+ }
+ }
+ }
+}
+
+func (c *Context) WriteBuildFile(w io.Writer) error {
+ if !c.buildActionsReady {
+ return ErrBuildActionsNotReady
+ }
+
+ nw := newNinjaWriter(w)
+
+ err := c.writeBuildFileHeader(nw)
+ if err != nil {
+ return err
+ }
+
+ err = c.writeNinjaRequiredVersion(nw)
+ if err != nil {
+ return err
+ }
+
+ // TODO: Group the globals by package.
+
+ err = c.writeGlobalVariables(nw)
+ if err != nil {
+ return err
+ }
+
+ err = c.writeGlobalPools(nw)
+ if err != nil {
+ return err
+ }
+
+ err = c.writeBuildDir(nw)
+ if err != nil {
+ return err
+ }
+
+ err = c.writeGlobalRules(nw)
+ if err != nil {
+ return err
+ }
+
+ err = c.writeAllModuleActions(nw)
+ if err != nil {
+ return err
+ }
+
+ err = c.writeAllSingletonActions(nw)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *Context) writeBuildFileHeader(nw *ninjaWriter) error {
+ headerTemplate := template.New("fileHeader")
+ _, err := headerTemplate.Parse(fileHeaderTemplate)
+ if err != nil {
+ // This is a programming error.
+ panic(err)
+ }
+
+ type pkgAssociation struct {
+ PkgName string
+ PkgPath string
+ }
+
+ var pkgs []pkgAssociation
+ maxNameLen := 0
+ for pkg, name := range c.pkgNames {
+ pkgs = append(pkgs, pkgAssociation{
+ PkgName: name,
+ PkgPath: pkg.pkgPath,
+ })
+ if len(name) > maxNameLen {
+ maxNameLen = len(name)
+ }
+ }
+
+ for i := range pkgs {
+ pkgs[i].PkgName += strings.Repeat(" ", maxNameLen-len(pkgs[i].PkgName))
+ }
+
+ params := map[string]interface{}{
+ "Pkgs": pkgs,
+ }
+
+ buf := bytes.NewBuffer(nil)
+ err = headerTemplate.Execute(buf, params)
+ if err != nil {
+ return err
+ }
+
+ return nw.Comment(buf.String())
+}
+
+func (c *Context) writeNinjaRequiredVersion(nw *ninjaWriter) error {
+ value := fmt.Sprintf("%d.%d.%d", c.requiredNinjaMajor, c.requiredNinjaMinor,
+ c.requiredNinjaMicro)
+
+ err := nw.Assign("ninja_required_version", value)
+ if err != nil {
+ return err
+ }
+
+ return nw.BlankLine()
+}
+
+func (c *Context) writeBuildDir(nw *ninjaWriter) error {
+ if c.buildDir != nil {
+ err := nw.Assign("builddir", c.buildDir.Value(c.pkgNames))
+ if err != nil {
+ return err
+ }
+
+ err = nw.BlankLine()
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+type variableSorter struct {
+ pkgNames map[*pkg]string
+ variables []Variable
+}
+
+func (v *variableSorter) Len() int {
+ return len(v.variables)
+}
+
+func (v *variableSorter) Less(i, j int) bool {
+ iName := v.variables[i].fullName(v.pkgNames)
+ jName := v.variables[j].fullName(v.pkgNames)
+ return iName < jName
+}
+
+func (v *variableSorter) Swap(i, j int) {
+ v.variables[i], v.variables[j] = v.variables[j], v.variables[i]
+}
+
+func (c *Context) writeGlobalVariables(nw *ninjaWriter) error {
+ visited := make(map[Variable]bool)
+
+ var walk func(v Variable) error
+ walk = func(v Variable) error {
+ visited[v] = true
+
+ // First visit variables on which this variable depends.
+ value := c.globalVariables[v]
+ for _, dep := range value.variables {
+ if !visited[dep] {
+ err := walk(dep)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ err := nw.Assign(v.fullName(c.pkgNames), value.Value(c.pkgNames))
+ if err != nil {
+ return err
+ }
+
+ err = nw.BlankLine()
+ if err != nil {
+ return err
+ }
+
+ return nil
+ }
+
+ globalVariables := make([]Variable, 0, len(c.globalVariables))
+ for v := range c.globalVariables {
+ globalVariables = append(globalVariables, v)
+ }
+
+ sort.Sort(&variableSorter{c.pkgNames, globalVariables})
+
+ for _, v := range globalVariables {
+ if !visited[v] {
+ err := walk(v)
+ if err != nil {
+ return nil
+ }
+ }
+ }
+
+ return nil
+}
+
+func (c *Context) writeGlobalPools(nw *ninjaWriter) error {
+ for pool, def := range c.globalPools {
+ name := pool.fullName(c.pkgNames)
+ err := def.WriteTo(nw, name)
+ if err != nil {
+ return err
+ }
+
+ err = nw.BlankLine()
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (c *Context) writeGlobalRules(nw *ninjaWriter) error {
+ for rule, def := range c.globalRules {
+ name := rule.fullName(c.pkgNames)
+ err := def.WriteTo(nw, name, c.pkgNames)
+ if err != nil {
+ return err
+ }
+
+ err = nw.BlankLine()
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (c *Context) writeAllModuleActions(nw *ninjaWriter) error {
+ headerTemplate := template.New("moduleHeader")
+ _, err := headerTemplate.Parse(moduleHeaderTemplate)
+ if err != nil {
+ // This is a programming error.
+ panic(err)
+ }
+
+ buf := bytes.NewBuffer(nil)
+
+ for _, info := range c.moduleInfo {
+ buf.Reset()
+ infoMap := map[string]interface{}{
+ "properties": info.properties,
+ "typeName": info.typeName,
+ "goTypeName": info.typ.name(),
+ "pos": info.pos,
+ }
+ err = headerTemplate.Execute(buf, infoMap)
+ if err != nil {
+ return err
+ }
+
+ err = nw.Comment(buf.String())
+ if err != nil {
+ return err
+ }
+
+ err = nw.BlankLine()
+ if err != nil {
+ return err
+ }
+
+ err = c.writeLocalBuildActions(nw, &info.actionDefs)
+ if err != nil {
+ return err
+ }
+
+ err = nw.BlankLine()
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (c *Context) writeAllSingletonActions(nw *ninjaWriter) error {
+ headerTemplate := template.New("singletonHeader")
+ _, err := headerTemplate.Parse(singletonHeaderTemplate)
+ if err != nil {
+ // This is a programming error.
+ panic(err)
+ }
+
+ buf := bytes.NewBuffer(nil)
+
+ for name, info := range c.singletonInfo {
+ buf.Reset()
+ infoMap := map[string]interface{}{
+ "name": name,
+ "goTypeName": singletonTypeName(info.singleton),
+ }
+ err = headerTemplate.Execute(buf, infoMap)
+ if err != nil {
+ return err
+ }
+
+ err = nw.Comment(buf.String())
+ if err != nil {
+ return err
+ }
+
+ err = nw.BlankLine()
+ if err != nil {
+ return err
+ }
+
+ err = c.writeLocalBuildActions(nw, &info.actionDefs)
+ if err != nil {
+ return err
+ }
+
+ err = nw.BlankLine()
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (c *Context) writeLocalBuildActions(nw *ninjaWriter,
+ defs *localBuildActions) error {
+
+ // Write the local variable assignments.
+ for _, v := range defs.variables {
+ // A localVariable doesn't need the package names or config to
+ // determine its name or value.
+ name := v.fullName(nil)
+ value, err := v.value(nil)
+ if err != nil {
+ panic(err)
+ }
+ err = nw.Assign(name, value.Value(c.pkgNames))
+ if err != nil {
+ return err
+ }
+ }
+
+ if len(defs.variables) > 0 {
+ err := nw.BlankLine()
+ if err != nil {
+ return err
+ }
+ }
+
+ // Write the local rules.
+ for _, r := range defs.rules {
+ // A localRule doesn't need the package names or config to determine
+ // its name or definition.
+ name := r.fullName(nil)
+ def, err := r.def(nil)
+ if err != nil {
+ panic(err)
+ }
+
+ err = def.WriteTo(nw, name, c.pkgNames)
+ if err != nil {
+ return err
+ }
+
+ err = nw.BlankLine()
+ if err != nil {
+ return err
+ }
+ }
+
+ // Write the build definitions.
+ for _, buildDef := range defs.buildDefs {
+ err := buildDef.WriteTo(nw, c.pkgNames)
+ if err != nil {
+ return err
+ }
+
+ if len(buildDef.Args) > 0 {
+ err = nw.BlankLine()
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+var fileHeaderTemplate = `******************************************************************************
+*** This file is generated and should not be edited ***
+******************************************************************************
+{{if .Pkgs}}
+This file contains variables, rules, and pools with name prefixes indicating
+they were generated by the following Go packages:
+{{range .Pkgs}}
+ {{.PkgName}} [from Go package {{.PkgPath}}]{{end}}{{end}}
+
+`
+
+var moduleHeaderTemplate = `# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+Module: {{.properties.Name}}
+Type: {{.typeName}}
+GoType: {{.goTypeName}}
+Defined: {{.pos}}
+`
+
+var singletonHeaderTemplate = `# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+Singleton: {{.name}}
+GoType: {{.goTypeName}}
+`
diff --git a/blueprint/context_test.go b/blueprint/context_test.go
new file mode 100644
index 0000000..2fda1b1
--- /dev/null
+++ b/blueprint/context_test.go
@@ -0,0 +1,91 @@
+package blueprint
+
+import (
+ "bytes"
+ "testing"
+)
+
+type fooModule struct {
+ properties struct {
+ Foo string
+ }
+}
+
+var FooModule = MakeModuleType("FooModule", newFooModule)
+
+func newFooModule() (Module, interface{}) {
+ m := &fooModule{}
+ return m, &m.properties
+}
+
+func (f *fooModule) GenerateBuildActions(ModuleContext) {
+}
+
+func (f *fooModule) Foo() string {
+ return f.properties.Foo
+}
+
+type barModule struct {
+ properties struct {
+ Bar bool
+ }
+}
+
+var BarModule = MakeModuleType("BarModule", newBarModule)
+
+func newBarModule() (Module, interface{}) {
+ m := &barModule{}
+ return m, &m.properties
+}
+
+func (b *barModule) GenerateBuildActions(ModuleContext) {
+}
+
+func (b *barModule) Bar() bool {
+ return b.properties.Bar
+}
+
+func TestContextParse(t *testing.T) {
+ ctx := NewContext()
+ ctx.RegisterModuleType("foo_module", FooModule)
+ ctx.RegisterModuleType("bar_module", BarModule)
+
+ r := bytes.NewBufferString(`
+ foo_module {
+ name: "MyFooModule",
+ deps: ["MyBarModule"],
+ }
+
+ bar_module {
+ name: "MyBarModule",
+ }
+ `)
+
+ _, errs := ctx.Parse(".", "Blueprint", r)
+ if len(errs) > 0 {
+ t.Errorf("unexpected parse errors:")
+ for _, err := range errs {
+ t.Errorf(" %s", err)
+ }
+ t.FailNow()
+ }
+
+ errs = ctx.resolveDependencies()
+ if len(errs) > 0 {
+ t.Errorf("unexpected dep errors:")
+ for _, err := range errs {
+ t.Errorf(" %s", err)
+ }
+ t.FailNow()
+ }
+
+ errs = ctx.checkForDependencyCycles()
+ if len(errs) > 0 {
+ t.Errorf("unexpected dep cycle errors:")
+ for _, err := range errs {
+ t.Errorf(" %s", err)
+ }
+ t.FailNow()
+ }
+
+}
diff --git a/blueprint/globals.go b/blueprint/globals.go
new file mode 100644
index 0000000..4a747df
--- /dev/null
+++ b/blueprint/globals.go
@@ -0,0 +1,533 @@
+package blueprint
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+ "runtime"
+ "strings"
+)
+
+type pkg struct {
+ fullName string
+ shortName string
+ pkgPath string
+ scope *scope
+}
+
+var pkgs = map[string]*pkg{}
+
+var pkgRegexp = regexp.MustCompile(`(.*)\.init(·[0-9]+)?`)
+
+var Phony Rule = &builtinRule{
+ name_: "phony",
+}
+
+var errRuleIsBuiltin = errors.New("the rule is a built-in")
+
+// We make a Ninja-friendly name out of a Go package name by replaceing all the
+// '/' characters with '.'. We assume the results are unique, though this is
+// not 100% guaranteed for Go package names that already contain '.' characters.
+// Disallowing package names with '.' isn't reasonable since many package names
+// contain the name of the hosting site (e.g. "code.google.com"). In practice
+// this probably isn't really a problem.
+func pkgPathToName(pkgPath string) string {
+ return strings.Replace(pkgPath, "/", ".", -1)
+}
+
+// callerPackage returns the pkg of the function that called the caller of
+// callerPackage. The caller of callerPackage must have been called from an
+// init function of the package or callerPackage will panic.
+//
+// Looking for the package's init function on the call stack and using that to
+// determine its package name is unfortunately dependent upon Go runtime
+// implementation details. However, it allows us to ensure that it's easy to
+// determine where a definition in a .ninja file came from.
+func callerPackage() *pkg {
+ var pc [1]uintptr
+ n := runtime.Callers(3, pc[:])
+ if n != 1 {
+ panic("unable to get caller pc")
+ }
+
+ f := runtime.FuncForPC(pc[0])
+ callerName := f.Name()
+
+ submatches := pkgRegexp.FindSubmatch([]byte(callerName))
+ if submatches == nil {
+ println(callerName)
+ panic("not called from an init func")
+ }
+
+ pkgPath := string(submatches[1])
+
+ pkgName := pkgPathToName(pkgPath)
+ err := validateNinjaName(pkgName)
+ if err != nil {
+ panic(err)
+ }
+
+ i := strings.LastIndex(pkgPath, "/")
+ shortName := pkgPath[i+1:]
+
+ p, ok := pkgs[pkgPath]
+ if !ok {
+ p = &pkg{
+ fullName: pkgName,
+ shortName: shortName,
+ pkgPath: pkgPath,
+ scope: newScope(nil),
+ }
+ pkgs[pkgPath] = p
+ }
+
+ return p
+}
+
+func Import(pkgPath string) {
+ callerPkg := callerPackage()
+
+ importPkg, ok := pkgs[pkgPath]
+ if !ok {
+ panic(fmt.Errorf("package %q has no Blueprints definitions", pkgPath))
+ }
+
+ err := callerPkg.scope.AddImport(importPkg.shortName, importPkg.scope)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func ImportAs(as, pkgPath string) {
+ callerPkg := callerPackage()
+
+ importPkg, ok := pkgs[pkgPath]
+ if !ok {
+ panic(fmt.Errorf("package %q has no Blueprints definitions", pkgPath))
+ }
+
+ err := validateNinjaName(as)
+ if err != nil {
+ panic(err)
+ }
+
+ err = callerPkg.scope.AddImport(as, importPkg.scope)
+ if err != nil {
+ panic(err)
+ }
+}
+
+type staticVariable struct {
+ pkg_ *pkg
+ name_ string
+ value_ string
+}
+
+// StaticVariable returns a Variable that does not depend on any configuration
+// information.
+func StaticVariable(name, value string) Variable {
+ err := validateNinjaName(name)
+ if err != nil {
+ panic(err)
+ }
+
+ pkg := callerPackage()
+
+ v := &staticVariable{pkg, name, value}
+ err = pkg.scope.AddVariable(v)
+ if err != nil {
+ panic(err)
+ }
+
+ return v
+}
+
+func (v *staticVariable) pkg() *pkg {
+ return v.pkg_
+}
+
+func (v *staticVariable) name() string {
+ return v.name_
+}
+
+func (v *staticVariable) fullName(pkgNames map[*pkg]string) string {
+ return packageNamespacePrefix(pkgNames[v.pkg_]) + v.name_
+}
+
+func (v *staticVariable) value(Config) (*ninjaString, error) {
+ return parseNinjaString(v.pkg_.scope, v.value_)
+}
+
+type variableFunc struct {
+ pkg_ *pkg
+ name_ string
+ value_ func(Config) (string, error)
+}
+
+// VariableFunc returns a Variable whose value is determined by a function that
+// takes a Config object as input and returns either the variable value or an
+// error.
+func VariableFunc(name string, f func(Config) (string, error)) Variable {
+ err := validateNinjaName(name)
+ if err != nil {
+ panic(err)
+ }
+
+ pkg := callerPackage()
+
+ v := &variableFunc{pkg, name, f}
+ err = pkg.scope.AddVariable(v)
+ if err != nil {
+ panic(err)
+ }
+
+ return v
+}
+
+func (v *variableFunc) pkg() *pkg {
+ return v.pkg_
+}
+
+func (v *variableFunc) name() string {
+ return v.name_
+}
+
+func (v *variableFunc) fullName(pkgNames map[*pkg]string) string {
+ return packageNamespacePrefix(pkgNames[v.pkg_]) + v.name_
+}
+
+func (v *variableFunc) value(config Config) (*ninjaString, error) {
+ value, err := v.value_(config)
+ if err != nil {
+ return nil, err
+ }
+ return parseNinjaString(v.pkg_.scope, value)
+}
+
+// An argVariable is a Variable that exists only when it is set by a build
+// statement to pass a value to the rule being invoked. It has no value, so it
+// can never be used to create a Ninja assignment statement. It is inserted
+// into the rule's scope, which is used for name lookups within the rule and
+// when assigning argument values as part of a build statement.
+type argVariable struct {
+ name_ string
+}
+
+func (v *argVariable) pkg() *pkg {
+ panic("this should not be called")
+}
+
+func (v *argVariable) name() string {
+ return v.name_
+}
+
+func (v *argVariable) fullName(pkgNames map[*pkg]string) string {
+ return v.name_
+}
+
+var errVariableIsArg = errors.New("argument variables have no value")
+
+func (v *argVariable) value(config Config) (*ninjaString, error) {
+ return nil, errVariableIsArg
+}
+
+type staticPool struct {
+ pkg_ *pkg
+ name_ string
+ params PoolParams
+}
+
+func StaticPool(name string, params PoolParams) Pool {
+ err := validateNinjaName(name)
+ if err != nil {
+ panic(err)
+ }
+
+ pkg := callerPackage()
+
+ p := &staticPool{pkg, name, params}
+ err = pkg.scope.AddPool(p)
+ if err != nil {
+ panic(err)
+ }
+
+ return p
+}
+
+func (p *staticPool) pkg() *pkg {
+ return p.pkg_
+}
+
+func (p *staticPool) name() string {
+ return p.name_
+}
+
+func (p *staticPool) fullName(pkgNames map[*pkg]string) string {
+ return packageNamespacePrefix(pkgNames[p.pkg_]) + p.name_
+}
+
+func (p *staticPool) def(config Config) (*poolDef, error) {
+ def, err := parsePoolParams(p.pkg_.scope, &p.params)
+ if err != nil {
+ panic(fmt.Errorf("error parsing PoolParams for %s: %s", p.name_, err))
+ }
+ return def, nil
+}
+
+type poolFunc struct {
+ pkg_ *pkg
+ name_ string
+ paramsFunc func(Config) (PoolParams, error)
+}
+
+func PoolFunc(name string, f func(Config) (PoolParams, error)) Pool {
+ err := validateNinjaName(name)
+ if err != nil {
+ panic(err)
+ }
+
+ pkg := callerPackage()
+
+ p := &poolFunc{pkg, name, f}
+ err = pkg.scope.AddPool(p)
+ if err != nil {
+ panic(err)
+ }
+
+ return p
+}
+
+func (p *poolFunc) pkg() *pkg {
+ return p.pkg_
+}
+
+func (p *poolFunc) name() string {
+ return p.name_
+}
+
+func (p *poolFunc) fullName(pkgNames map[*pkg]string) string {
+ return packageNamespacePrefix(pkgNames[p.pkg_]) + p.name_
+}
+
+func (p *poolFunc) def(config Config) (*poolDef, error) {
+ params, err := p.paramsFunc(config)
+ if err != nil {
+ return nil, err
+ }
+ def, err := parsePoolParams(p.pkg_.scope, ¶ms)
+ if err != nil {
+ panic(fmt.Errorf("error parsing PoolParams for %s: %s", p.name_, err))
+ }
+ return def, nil
+}
+
+type staticRule struct {
+ pkg_ *pkg
+ name_ string
+ params RuleParams
+ argNames map[string]bool
+ scope_ *scope
+}
+
+func StaticRule(name string, params RuleParams, argNames ...string) Rule {
+ pkg := callerPackage()
+
+ err := validateNinjaName(name)
+ if err != nil {
+ panic(err)
+ }
+
+ err = validateArgNames(argNames)
+ if err != nil {
+ panic(fmt.Errorf("invalid argument name: %s", err))
+ }
+
+ argNamesSet := make(map[string]bool)
+ for _, argName := range argNames {
+ argNamesSet[argName] = true
+ }
+
+ ruleScope := (*scope)(nil) // This will get created lazily
+
+ r := &staticRule{pkg, name, params, argNamesSet, ruleScope}
+ err = pkg.scope.AddRule(r)
+ if err != nil {
+ panic(err)
+ }
+
+ return r
+}
+
+func (r *staticRule) pkg() *pkg {
+ return r.pkg_
+}
+
+func (r *staticRule) name() string {
+ return r.name_
+}
+
+func (r *staticRule) fullName(pkgNames map[*pkg]string) string {
+ return packageNamespacePrefix(pkgNames[r.pkg_]) + r.name_
+}
+
+func (r *staticRule) def(Config) (*ruleDef, error) {
+ def, err := parseRuleParams(r.scope(), &r.params)
+ if err != nil {
+ panic(fmt.Errorf("error parsing RuleParams for %s: %s", r.name_, err))
+ }
+ return def, nil
+}
+
+func (r *staticRule) scope() *scope {
+ // We lazily create the scope so that all the global variables get declared
+ // before the args are created. Otherwise we could incorrectly shadow a
+ // global variable with an arg variable.
+ if r.scope_ == nil {
+ r.scope_ = makeRuleScope(r.pkg_.scope, r.argNames)
+ }
+ return r.scope_
+}
+
+func (r *staticRule) isArg(argName string) bool {
+ return r.argNames[argName]
+}
+
+type ruleFunc struct {
+ pkg_ *pkg
+ name_ string
+ paramsFunc func(Config) (RuleParams, error)
+ argNames map[string]bool
+ scope_ *scope
+}
+
+func RuleFunc(name string, f func(Config) (RuleParams, error),
+ argNames ...string) Rule {
+
+ pkg := callerPackage()
+
+ err := validateNinjaName(name)
+ if err != nil {
+ panic(err)
+ }
+
+ err = validateArgNames(argNames)
+ if err != nil {
+ panic(fmt.Errorf("invalid argument name: %s", err))
+ }
+
+ argNamesSet := make(map[string]bool)
+ for _, argName := range argNames {
+ argNamesSet[argName] = true
+ }
+
+ ruleScope := (*scope)(nil) // This will get created lazily
+
+ r := &ruleFunc{pkg, name, f, argNamesSet, ruleScope}
+ err = pkg.scope.AddRule(r)
+ if err != nil {
+ panic(err)
+ }
+
+ return r
+}
+
+func (r *ruleFunc) pkg() *pkg {
+ return r.pkg_
+}
+
+func (r *ruleFunc) name() string {
+ return r.name_
+}
+
+func (r *ruleFunc) fullName(pkgNames map[*pkg]string) string {
+ return packageNamespacePrefix(pkgNames[r.pkg_]) + r.name_
+}
+
+func (r *ruleFunc) def(config Config) (*ruleDef, error) {
+ params, err := r.paramsFunc(config)
+ if err != nil {
+ return nil, err
+ }
+ def, err := parseRuleParams(r.scope(), ¶ms)
+ if err != nil {
+ panic(fmt.Errorf("error parsing RuleParams for %s: %s", r.name_, err))
+ }
+ return def, nil
+}
+
+func (r *ruleFunc) scope() *scope {
+ // We lazily create the scope so that all the global variables get declared
+ // before the args are created. Otherwise we could incorrectly shadow a
+ // global variable with an arg variable.
+ if r.scope_ == nil {
+ r.scope_ = makeRuleScope(r.pkg_.scope, r.argNames)
+ }
+ return r.scope_
+}
+
+func (r *ruleFunc) isArg(argName string) bool {
+ return r.argNames[argName]
+}
+
+type builtinRule struct {
+ name_ string
+ scope_ *scope
+}
+
+func (r *builtinRule) pkg() *pkg {
+ return nil
+}
+
+func (r *builtinRule) name() string {
+ return r.name_
+}
+
+func (r *builtinRule) fullName(pkgNames map[*pkg]string) string {
+ return r.name_
+}
+
+func (r *builtinRule) def(config Config) (*ruleDef, error) {
+ return nil, errRuleIsBuiltin
+}
+
+func (r *builtinRule) scope() *scope {
+ if r.scope_ == nil {
+ r.scope_ = makeRuleScope(nil, nil)
+ }
+ return r.scope_
+}
+
+func (r *builtinRule) isArg(argName string) bool {
+ return false
+}
+
+type ModuleType interface {
+ pkg() *pkg
+ name() string
+ new() (m Module, properties interface{})
+}
+
+type moduleTypeFunc struct {
+ pkg_ *pkg
+ name_ string
+ new_ func() (Module, interface{})
+}
+
+func MakeModuleType(name string,
+ new func() (m Module, properties interface{})) ModuleType {
+
+ pkg := callerPackage()
+ return &moduleTypeFunc{pkg, name, new}
+}
+
+func (m *moduleTypeFunc) pkg() *pkg {
+ return m.pkg_
+}
+
+func (m *moduleTypeFunc) name() string {
+ return m.pkg_.pkgPath + "." + m.name_
+}
+
+func (m *moduleTypeFunc) new() (Module, interface{}) {
+ return m.new_()
+}
diff --git a/blueprint/live_tracker.go b/blueprint/live_tracker.go
new file mode 100644
index 0000000..bad299a
--- /dev/null
+++ b/blueprint/live_tracker.go
@@ -0,0 +1,149 @@
+package blueprint
+
+// A liveTracker tracks the values of live variables, rules, and pools. An
+// entity is made "live" when it is referenced directly or indirectly by a build
+// definition. When an entity is made live its value is computed based on the
+// configuration.
+type liveTracker struct {
+ config Config // Used to evaluate variable, rule, and pool values.
+
+ variables map[Variable]*ninjaString
+ pools map[Pool]*poolDef
+ rules map[Rule]*ruleDef
+}
+
+func newLiveTracker(config Config) *liveTracker {
+ return &liveTracker{
+ config: config,
+ variables: make(map[Variable]*ninjaString),
+ pools: make(map[Pool]*poolDef),
+ rules: make(map[Rule]*ruleDef),
+ }
+}
+
+func (l *liveTracker) AddBuildDefDeps(def *buildDef) error {
+ err := l.addRule(def.Rule)
+ if err != nil {
+ return err
+ }
+
+ err = l.addNinjaStringListDeps(def.Outputs)
+ if err != nil {
+ return err
+ }
+
+ err = l.addNinjaStringListDeps(def.Inputs)
+ if err != nil {
+ return err
+ }
+
+ err = l.addNinjaStringListDeps(def.Implicits)
+ if err != nil {
+ return err
+ }
+
+ err = l.addNinjaStringListDeps(def.OrderOnly)
+ if err != nil {
+ return err
+ }
+
+ for _, value := range def.Args {
+ err = l.addNinjaStringDeps(value)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (l *liveTracker) addRule(r Rule) error {
+ _, ok := l.rules[r]
+ if !ok {
+ def, err := r.def(l.config)
+ if err == errRuleIsBuiltin {
+ // No need to do anything for built-in rules.
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+
+ if def.Pool != nil {
+ err = l.addPool(def.Pool)
+ if err != nil {
+ return err
+ }
+ }
+
+ for _, value := range def.Variables {
+ err = l.addNinjaStringDeps(value)
+ if err != nil {
+ return err
+ }
+ }
+
+ l.rules[r] = def
+ }
+
+ return nil
+}
+
+func (l *liveTracker) addPool(p Pool) error {
+ _, ok := l.pools[p]
+ if !ok {
+ def, err := p.def(l.config)
+ if err != nil {
+ return err
+ }
+
+ l.pools[p] = def
+ }
+
+ return nil
+}
+
+func (l *liveTracker) addVariable(v Variable) error {
+ _, ok := l.variables[v]
+ if !ok {
+ value, err := v.value(l.config)
+ if err == errVariableIsArg {
+ // This variable is a placeholder for an argument that can be passed
+ // to a rule. It has no value and thus doesn't reference any other
+ // variables.
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+
+ l.variables[v] = value
+
+ err = l.addNinjaStringDeps(value)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (l *liveTracker) addNinjaStringListDeps(list []*ninjaString) error {
+ for _, str := range list {
+ err := l.addNinjaStringDeps(str)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (l *liveTracker) addNinjaStringDeps(str *ninjaString) error {
+ for _, v := range str.variables {
+ err := l.addVariable(v)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/blueprint/mangle.go b/blueprint/mangle.go
new file mode 100644
index 0000000..22eec37
--- /dev/null
+++ b/blueprint/mangle.go
@@ -0,0 +1,13 @@
+package blueprint
+
+func packageNamespacePrefix(packageName string) string {
+ return "g." + packageName + "."
+}
+
+func moduleNamespacePrefix(moduleName string) string {
+ return "m." + moduleName + "."
+}
+
+func singletonNamespacePrefix(singletonName string) string {
+ return "s." + singletonName + "."
+}
diff --git a/blueprint/module_ctx.go b/blueprint/module_ctx.go
new file mode 100644
index 0000000..f8fe564
--- /dev/null
+++ b/blueprint/module_ctx.go
@@ -0,0 +1,116 @@
+package blueprint
+
+import (
+ "fmt"
+ "path/filepath"
+)
+
+type Module interface {
+ GenerateBuildActions(ModuleContext)
+}
+
+type ModuleContext interface {
+ ModuleName() string
+ ModuleDir() string
+ Config() Config
+
+ ModuleErrorf(fmt string, args ...interface{})
+ PropertyErrorf(property, fmt string, args ...interface{})
+
+ Variable(name, value string)
+ Rule(name string, params RuleParams) Rule
+ Build(params BuildParams)
+
+ VisitDepsDepthFirst(visit func(Module))
+ VisitDepsDepthFirstIf(pred func(Module) bool, visit func(Module))
+}
+
+var _ ModuleContext = (*moduleContext)(nil)
+
+type moduleContext struct {
+ context *Context
+ config Config
+ module Module
+ scope *localScope
+ info *moduleInfo
+
+ errs []error
+
+ actionDefs localBuildActions
+}
+
+func (m *moduleContext) ModuleName() string {
+ return m.info.properties.Name
+}
+
+func (m *moduleContext) ModuleDir() string {
+ return filepath.Dir(m.info.relBlueprintFile)
+}
+
+func (m *moduleContext) Config() Config {
+ return m.config
+}
+
+func (m *moduleContext) ModuleErrorf(format string, args ...interface{}) {
+ m.errs = append(m.errs, &Error{
+ Err: fmt.Errorf(format, args...),
+ Pos: m.info.pos,
+ })
+}
+
+func (m *moduleContext) PropertyErrorf(property, format string,
+ args ...interface{}) {
+
+ pos, ok := m.info.propertyPos[property]
+ if !ok {
+ panic(fmt.Errorf("property %q was not set for this module", property))
+ }
+
+ m.errs = append(m.errs, &Error{
+ Err: fmt.Errorf(format, args...),
+ Pos: pos,
+ })
+}
+
+func (m *moduleContext) Variable(name, value string) {
+ v, err := m.scope.AddLocalVariable(name, value)
+ if err != nil {
+ panic(err)
+ }
+
+ m.actionDefs.variables = append(m.actionDefs.variables, v)
+}
+
+func (m *moduleContext) Rule(name string, params RuleParams) Rule {
+ // TODO: Verify that params.Pool is accessible in this module's scope.
+
+ r, err := m.scope.AddLocalRule(name, ¶ms)
+ if err != nil {
+ panic(err)
+ }
+
+ m.actionDefs.rules = append(m.actionDefs.rules, r)
+
+ return r
+}
+
+func (m *moduleContext) Build(params BuildParams) {
+ // TODO: Verify that params.Rule is accessible in this module's scope.
+
+ def, err := parseBuildParams(m.scope, ¶ms)
+ if err != nil {
+ panic(err)
+ }
+
+ m.actionDefs.buildDefs = append(m.actionDefs.buildDefs, def)
+}
+
+func (m *moduleContext) VisitDepsDepthFirst(visit func(Module)) {
+ m.context.visitDepsDepthFirst(m.module, visit)
+}
+
+func (m *moduleContext) VisitDepsDepthFirstIf(pred func(Module) bool,
+ visit func(Module)) {
+
+ m.context.visitDepsDepthFirstIf(m.module, pred, visit)
+}
diff --git a/blueprint/ninja_defs.go b/blueprint/ninja_defs.go
new file mode 100644
index 0000000..3f3329d
--- /dev/null
+++ b/blueprint/ninja_defs.go
@@ -0,0 +1,299 @@
+package blueprint
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+type Deps int
+
+const (
+ DepsNone Deps = iota
+ DepsGCC
+ DepsMSVC
+)
+
+func (d Deps) String() string {
+ switch d {
+ case DepsNone:
+ return "none"
+ case DepsGCC:
+ return "gcc"
+ case DepsMSVC:
+ return "msvc"
+ default:
+ panic(fmt.Sprintf("unknown deps value: %d", d))
+ }
+}
+
+type PoolParams struct {
+ Comment string
+ Depth int
+}
+
+type RuleParams struct {
+ Comment string
+ Command string
+ Depfile string
+ Deps Deps
+ Description string
+ Generator bool
+ Pool Pool
+ Restat bool
+ Rspfile string
+ RspfileContent string
+}
+
+type BuildParams struct {
+ Rule Rule
+ Outputs []string
+ Inputs []string
+ Implicits []string
+ OrderOnly []string
+ Args map[string]string
+}
+
+// A poolDef describes a pool definition. It does not include the name of the
+// pool.
+type poolDef struct {
+ Comment string
+ Depth int
+}
+
+func parsePoolParams(scope variableLookup, params *PoolParams) (*poolDef,
+ error) {
+
+ def := &poolDef{
+ Comment: params.Comment,
+ Depth: params.Depth,
+ }
+
+ return def, nil
+}
+
+func (p *poolDef) WriteTo(nw *ninjaWriter, name string) error {
+ if p.Comment != "" {
+ err := nw.Comment(p.Comment)
+ if err != nil {
+ return err
+ }
+ }
+
+ err := nw.Pool(name)
+ if err != nil {
+ return err
+ }
+
+ return nw.ScopedAssign("depth", strconv.Itoa(p.Depth))
+}
+
+// A ruleDef describes a rule definition. It does not include the name of the
+// rule.
+type ruleDef struct {
+ Comment string
+ Pool Pool
+ Variables map[string]*ninjaString
+}
+
+func parseRuleParams(scope variableLookup, params *RuleParams) (*ruleDef,
+ error) {
+
+ r := &ruleDef{
+ Comment: params.Comment,
+ Pool: params.Pool,
+ Variables: make(map[string]*ninjaString),
+ }
+
+ if params.Command == "" {
+ return nil, fmt.Errorf("encountered rule params with no command " +
+ "specified")
+ }
+
+ value, err := parseNinjaString(scope, params.Command)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing Command param: %s", err)
+ }
+ r.Variables["command"] = value
+
+ if params.Depfile != "" {
+ value, err = parseNinjaString(scope, params.Depfile)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing Depfile param: %s", err)
+ }
+ r.Variables["depfile"] = value
+ }
+
+ if params.Deps != DepsNone {
+ r.Variables["deps"] = simpleNinjaString(params.Deps.String())
+ }
+
+ if params.Description != "" {
+ value, err = parseNinjaString(scope, params.Description)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing Description param: %s", err)
+ }
+ r.Variables["description"] = value
+ }
+
+ if params.Generator {
+ r.Variables["generator"] = simpleNinjaString("true")
+ }
+
+ if params.Restat {
+ r.Variables["restat"] = simpleNinjaString("true")
+ }
+
+ if params.Rspfile != "" {
+ value, err = parseNinjaString(scope, params.Rspfile)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing Rspfile param: %s", err)
+ }
+ r.Variables["rspfile"] = value
+ }
+
+ if params.RspfileContent != "" {
+ value, err = parseNinjaString(scope, params.RspfileContent)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing RspfileContent param: %s",
+ err)
+ }
+ r.Variables["rspfile_content"] = value
+ }
+
+ return r, nil
+}
+
+func (r *ruleDef) WriteTo(nw *ninjaWriter, name string,
+ pkgNames map[*pkg]string) error {
+
+ if r.Comment != "" {
+ err := nw.Comment(r.Comment)
+ if err != nil {
+ return err
+ }
+ }
+
+ err := nw.Rule(name)
+ if err != nil {
+ return err
+ }
+
+ if r.Pool != nil {
+ err = nw.ScopedAssign("pool", r.Pool.fullName(pkgNames))
+ if err != nil {
+ return err
+ }
+ }
+
+ for name, value := range r.Variables {
+ err = nw.ScopedAssign(name, value.Value(pkgNames))
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// A buildDef describes a build target definition.
+type buildDef struct {
+ Rule Rule
+ Outputs []*ninjaString
+ Inputs []*ninjaString
+ Implicits []*ninjaString
+ OrderOnly []*ninjaString
+ Args map[Variable]*ninjaString
+}
+
+func parseBuildParams(scope variableLookup, params *BuildParams) (*buildDef,
+ error) {
+
+ rule := params.Rule
+
+ b := &buildDef{
+ Rule: rule,
+ }
+
+ var err error
+ b.Outputs, err = parseNinjaStrings(scope, params.Outputs)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing Outputs param: %s", err)
+ }
+
+ b.Inputs, err = parseNinjaStrings(scope, params.Inputs)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing Inputs param: %s", err)
+ }
+
+ b.Implicits, err = parseNinjaStrings(scope, params.Implicits)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing Implicits param: %s", err)
+ }
+
+ b.OrderOnly, err = parseNinjaStrings(scope, params.OrderOnly)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing OrderOnly param: %s", err)
+ }
+
+ argNameScope := rule.scope()
+
+ if len(params.Args) > 0 {
+ b.Args = make(map[Variable]*ninjaString)
+ for name, value := range params.Args {
+ if !rule.isArg(name) {
+ return nil, fmt.Errorf("unknown argument %q", name)
+ }
+
+ argVar, err := argNameScope.LookupVariable(name)
+ if err != nil {
+ // This shouldn't happen.
+ return nil, fmt.Errorf("argument lookup error: %s", err)
+ }
+
+ ninjaValue, err := parseNinjaString(scope, value)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing variable %q: %s", name,
+ err)
+ }
+
+ b.Args[argVar] = ninjaValue
+ }
+ }
+
+ return b, nil
+}
+
+func (b *buildDef) WriteTo(nw *ninjaWriter, pkgNames map[*pkg]string) error {
+ var (
+ rule = b.Rule.fullName(pkgNames)
+ outputs = valueList(b.Outputs, pkgNames, outputEscaper)
+ explicitDeps = valueList(b.Inputs, pkgNames, inputEscaper)
+ implicitDeps = valueList(b.Implicits, pkgNames, inputEscaper)
+ orderOnlyDeps = valueList(b.OrderOnly, pkgNames, inputEscaper)
+ )
+ err := nw.Build(rule, outputs, explicitDeps, implicitDeps, orderOnlyDeps)
+ if err != nil {
+ return err
+ }
+
+ for argVar, value := range b.Args {
+ name := argVar.fullName(pkgNames)
+ err = nw.ScopedAssign(name, value.Value(pkgNames))
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func valueList(list []*ninjaString, pkgNames map[*pkg]string,
+ escaper *strings.Replacer) []string {
+
+ result := make([]string, len(list))
+ for i, ninjaStr := range list {
+ result[i] = ninjaStr.ValueWithEscaper(pkgNames, escaper)
+ }
+ return result
+}
diff --git a/blueprint/ninja_strings.go b/blueprint/ninja_strings.go
new file mode 100644
index 0000000..035d084
--- /dev/null
+++ b/blueprint/ninja_strings.go
@@ -0,0 +1,281 @@
+package blueprint
+
+import (
+ "fmt"
+ "strings"
+)
+
+const EOF = -1
+
+var (
+ defaultEscaper = strings.NewReplacer(
+ "\n", "$\n")
+ inputEscaper = strings.NewReplacer(
+ "\n", "$\n",
+ " ", "$ ")
+ outputEscaper = strings.NewReplacer(
+ "\n", "$\n",
+ " ", "$ ",
+ ":", "$:")
+)
+
+type ninjaString struct {
+ strings []string
+ variables []Variable
+}
+
+type variableLookup interface {
+ LookupVariable(name string) (Variable, error)
+}
+
+func simpleNinjaString(str string) *ninjaString {
+ return &ninjaString{
+ strings: []string{str},
+ }
+}
+
+// parseNinjaString parses an unescaped ninja string (i.e. all $<something>
+// occurrences are expected to be variables or $$) and returns a list of the
+// variable names that the string references.
+func parseNinjaString(scope variableLookup, str string) (*ninjaString, error) {
+ type stateFunc func(int, rune) (stateFunc, error)
+ var (
+ stringState stateFunc
+ dollarStartState stateFunc
+ dollarState stateFunc
+ bracketsState stateFunc
+ )
+
+ var stringStart, varStart int
+ var result ninjaString
+
+ stringState = func(i int, r rune) (stateFunc, error) {
+ switch {
+ case r == '$':
+ varStart = i + 1
+ return dollarStartState, nil
+
+ case r == EOF:
+ result.strings = append(result.strings, str[stringStart:i])
+ return nil, nil
+
+ default:
+ return stringState, nil
+ }
+ }
+
+ dollarStartState = func(i int, r rune) (stateFunc, error) {
+ switch {
+ case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
+ r >= '0' && r <= '9', r == '_', r == '-':
+ // The beginning of a of the variable name. Output the string and
+ // keep going.
+ result.strings = append(result.strings, str[stringStart:i-1])
+ return dollarState, nil
+
+ case r == '$':
+ // Just a "$$". Go back to stringState without changing
+ // stringStart.
+ return stringState, nil
+
+ case r == '{':
+ // This is a bracketted variable name (e.g. "${blah.blah}"). Output
+ // the string and keep going.
+ result.strings = append(result.strings, str[stringStart:i-1])
+ varStart = i + 1
+ return bracketsState, nil
+
+ case r == EOF:
+ return nil, fmt.Errorf("unexpected end of string after '$'")
+
+ default:
+ // This was some arbitrary character following a dollar sign,
+ // which is not allowed.
+ return nil, fmt.Errorf("invalid character after '$' at byte "+
+ "offset %d", i)
+ }
+ }
+
+ dollarState = func(i int, r rune) (stateFunc, error) {
+ switch {
+ case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
+ r >= '0' && r <= '9', r == '_', r == '-':
+ // A part of the variable name. Keep going.
+ return dollarState, nil
+
+ case r == '$':
+ // A dollar after the variable name (e.g. "$blah$"). Output the
+ // variable we have and start a new one.
+ v, err := scope.LookupVariable(str[varStart:i])
+ if err != nil {
+ return nil, err
+ }
+
+ result.variables = append(result.variables, v)
+ varStart = i + 1
+ return dollarState, nil
+
+ case r == EOF:
+ // This is the end of the variable name.
+ v, err := scope.LookupVariable(str[varStart:i])
+ if err != nil {
+ return nil, err
+ }
+
+ result.variables = append(result.variables, v)
+
+ // We always end with a string, even if it's an empty one.
+ result.strings = append(result.strings, "")
+
+ return nil, nil
+
+ default:
+ // We've just gone past the end of the variable name, so record what
+ // we have.
+ v, err := scope.LookupVariable(str[varStart:i])
+ if err != nil {
+ return nil, err
+ }
+
+ result.variables = append(result.variables, v)
+ stringStart = i
+ return stringState, nil
+ }
+ }
+
+ bracketsState = func(i int, r rune) (stateFunc, error) {
+ switch {
+ case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
+ r >= '0' && r <= '9', r == '_', r == '-', r == '.':
+ // A part of the variable name. Keep going.
+ return bracketsState, nil
+
+ case r == '}':
+ if varStart == i {
+ // The brackets were immediately closed. That's no good.
+ return nil, fmt.Errorf("empty variable name at byte offset %d",
+ i)
+ }
+
+ // This is the end of the variable name.
+ v, err := scope.LookupVariable(str[varStart:i])
+ if err != nil {
+ return nil, err
+ }
+
+ result.variables = append(result.variables, v)
+ stringStart = i + 1
+ return stringState, nil
+
+ case r == EOF:
+ return nil, fmt.Errorf("unexpected end of string in variable name")
+
+ default:
+ // This character isn't allowed in a variable name.
+ return nil, fmt.Errorf("invalid character in variable name at "+
+ "byte offset %d", i)
+ }
+ }
+
+ state := stringState
+ var err error
+ for i, r := range str {
+ state, err = state(i, r)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ _, err = state(len(str), EOF)
+ if err != nil {
+ return nil, err
+ }
+
+ return &result, nil
+}
+
+func parseNinjaStrings(scope variableLookup, strs []string) ([]*ninjaString,
+ error) {
+
+ if len(strs) == 0 {
+ return nil, nil
+ }
+ result := make([]*ninjaString, len(strs))
+ for i, str := range strs {
+ ninjaStr, err := parseNinjaString(scope, str)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing element %d: %s", i, err)
+ }
+ result[i] = ninjaStr
+ }
+ return result, nil
+}
+
+func (n *ninjaString) Value(pkgNames map[*pkg]string) string {
+ return n.ValueWithEscaper(pkgNames, defaultEscaper)
+}
+
+func (n *ninjaString) ValueWithEscaper(pkgNames map[*pkg]string,
+ escaper *strings.Replacer) string {
+
+ str := escaper.Replace(n.strings[0])
+ for i, v := range n.variables {
+ str += "${" + v.fullName(pkgNames) + "}"
+ str += escaper.Replace(n.strings[i+1])
+ }
+ return str
+}
+
+func validateNinjaName(name string) error {
+ for i, r := range name {
+ valid := (r >= 'a' && r <= 'z') ||
+ (r >= 'A' && r <= 'Z') ||
+ (r >= '0' && r <= '9') ||
+ (r == '_') ||
+ (r == '-') ||
+ (r == '.')
+ if !valid {
+ return fmt.Errorf("%q contains an invalid Ninja name character "+
+ "at byte offset %d", name, i)
+ }
+ }
+ return nil
+}
+
+var builtinRuleArgs = []string{"out", "in"}
+
+func validateArgName(argName string) error {
+ err := validateNinjaName(argName)
+ if err != nil {
+ return err
+ }
+
+ // We only allow globals within the rule's package to be used as rule
+ // arguments. A global in another package can always be mirrored into
+ // the rule's package by defining a new variable, so this doesn't limit
+ // what's possible. This limitation prevents situations where a Build
+ // invocation in another package must use the rule-defining package's
+ // import name for a 3rd package in order to set the rule's arguments.
+ if strings.ContainsRune(argName, '.') {
+ return fmt.Errorf("%q contains a '.' character", argName)
+ }
+
+ for _, builtin := range builtinRuleArgs {
+ if argName == builtin {
+ return fmt.Errorf("%q conflicts with Ninja built-in", argName)
+ }
+ }
+
+ return nil
+}
+
+func validateArgNames(argNames []string) error {
+ for _, argName := range argNames {
+ err := validateArgName(argName)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/blueprint/ninja_strings_test.go b/blueprint/ninja_strings_test.go
new file mode 100644
index 0000000..db47803
--- /dev/null
+++ b/blueprint/ninja_strings_test.go
@@ -0,0 +1,114 @@
+package blueprint
+
+import (
+ "reflect"
+ "testing"
+)
+
+var ninjaParseTestCases = []struct {
+ input string
+ output []string
+ err string
+}{
+ {
+ input: "abc def $ghi jkl",
+ output: []string{"ghi"},
+ },
+ {
+ input: "abc def $ghi$jkl",
+ output: []string{"ghi", "jkl"},
+ },
+ {
+ input: "foo $012_-345xyz_! bar",
+ output: []string{"012_-345xyz_"},
+ },
+ {
+ input: "foo ${012_-345xyz_} bar",
+ output: []string{"012_-345xyz_"},
+ },
+ {
+ input: "foo ${012_-345xyz_} bar",
+ output: []string{"012_-345xyz_"},
+ },
+ {
+ input: "foo $$ bar",
+ output: nil,
+ },
+ {
+ input: "foo $ bar",
+ err: "invalid character after '$' at byte offset 5",
+ },
+ {
+ input: "foo $",
+ err: "unexpected end of string after '$'",
+ },
+ {
+ input: "foo ${} bar",
+ err: "empty variable name at byte offset 6",
+ },
+ {
+ input: "foo ${abc!} bar",
+ err: "invalid character in variable name at byte offset 9",
+ },
+ {
+ input: "foo ${abc",
+ err: "unexpected end of string in variable name",
+ },
+}
+
+func TestParseNinjaString(t *testing.T) {
+ for _, testCase := range ninjaParseTestCases {
+ scope := newLocalScope(nil, "namespace")
+ var expectedVars []Variable
+ for _, varName := range testCase.output {
+ v, err := scope.LookupVariable(varName)
+ if err != nil {
+ v, err = scope.AddLocalVariable(varName, "")
+ if err != nil {
+ t.Fatalf("error creating scope: %s", err)
+ }
+ }
+ expectedVars = append(expectedVars, v)
+ }
+
+ output, err := parseNinjaString(scope, testCase.input)
+ if err == nil && !reflect.DeepEqual(output.variables, expectedVars) {
+ t.Errorf("incorrect output:")
+ t.Errorf(" input: %q", testCase.input)
+ t.Errorf(" expected: %#v", testCase.output)
+ t.Errorf(" got: %#v", output)
+ }
+ var errStr string
+ if err != nil {
+ errStr = err.Error()
+ }
+ if err != nil && err.Error() != testCase.err {
+ t.Errorf("unexpected error:")
+ t.Errorf(" input: %q", testCase.input)
+ t.Errorf(" expected: %q", testCase.err)
+ t.Errorf(" got: %q", errStr)
+ }
+ }
+}
+
+func TestParseNinjaStringWithImportedVar(t *testing.T) {
+ ImpVar := &staticVariable{name_: "ImpVar"}
+ impScope := newScope(nil)
+ impScope.AddVariable(ImpVar)
+ scope := newScope(nil)
+ scope.AddImport("impPkg", impScope)
+
+ input := "abc def ${impPkg.ImpVar} ghi"
+ output, err := parseNinjaString(scope, input)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+
+ expect := []Variable{ImpVar}
+ if !reflect.DeepEqual(output.variables, expect) {
+ t.Errorf("incorrect output:")
+ t.Errorf(" input: %q", input)
+ t.Errorf(" expected: %#v", expect)
+ t.Errorf(" got: %#v", output)
+ }
+}
diff --git a/blueprint/ninja_writer.go b/blueprint/ninja_writer.go
new file mode 100644
index 0000000..0051969
--- /dev/null
+++ b/blueprint/ninja_writer.go
@@ -0,0 +1,244 @@
+package blueprint
+
+import (
+ "fmt"
+ "io"
+ "strings"
+ "unicode"
+)
+
+const (
+ indentWidth = 4
+ lineWidth = 80
+)
+
+type ninjaWriter struct {
+ writer io.Writer
+
+ justDidBlankLine bool // true if the last operation was a BlankLine
+}
+
+func newNinjaWriter(writer io.Writer) *ninjaWriter {
+ return &ninjaWriter{
+ writer: writer,
+ }
+}
+
+func (n *ninjaWriter) Comment(comment string) error {
+ n.justDidBlankLine = false
+
+ const lineHeaderLen = len("# ")
+ const maxLineLen = lineWidth - lineHeaderLen
+
+ var lineStart, lastSplitPoint int
+ for i, r := range comment {
+ if unicode.IsSpace(r) {
+ // We know we can safely split the line here.
+ lastSplitPoint = i + 1
+ }
+
+ var line string
+ var writeLine bool
+ switch {
+ case r == '\n':
+ // Output the line without trimming the left so as to allow comments
+ // to contain their own indentation.
+ line = strings.TrimRightFunc(comment[lineStart:i], unicode.IsSpace)
+ writeLine = true
+
+ case (i-lineStart > maxLineLen) && (lastSplitPoint > lineStart):
+ // The line has grown too long and is splittable. Split it at the
+ // last split point.
+ line = strings.TrimSpace(comment[lineStart:lastSplitPoint])
+ writeLine = true
+ }
+
+ if writeLine {
+ line = strings.TrimSpace("# "+line) + "\n"
+ _, err := io.WriteString(n.writer, line)
+ if err != nil {
+ return err
+ }
+ lineStart = lastSplitPoint
+ }
+ }
+
+ if lineStart != len(comment) {
+ line := strings.TrimSpace(comment[lineStart:])
+ _, err := fmt.Fprintf(n.writer, "# %s\n", line)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (n *ninjaWriter) Pool(name string) error {
+ n.justDidBlankLine = false
+ _, err := fmt.Fprintf(n.writer, "pool %s\n", name)
+ return err
+}
+
+func (n *ninjaWriter) Rule(name string) error {
+ n.justDidBlankLine = false
+ _, err := fmt.Fprintf(n.writer, "rule %s\n", name)
+ return err
+}
+
+func (n *ninjaWriter) Build(rule string, outputs, explicitDeps, implicitDeps,
+ orderOnlyDeps []string) error {
+
+ n.justDidBlankLine = false
+
+ const lineWrapLen = len(" $")
+ const maxLineLen = lineWidth - lineWrapLen
+
+ line := "build"
+
+ appendWithWrap := func(s string) (err error) {
+ if len(line)+len(s) > maxLineLen {
+ _, err = fmt.Fprintf(n.writer, "%s $\n", line)
+ line = strings.Repeat(" ", indentWidth*2)
+ s = strings.TrimLeftFunc(s, unicode.IsSpace)
+ }
+ line += s
+ return
+ }
+
+ for _, output := range outputs {
+ err := appendWithWrap(" " + output)
+ if err != nil {
+ return err
+ }
+ }
+
+ err := appendWithWrap(":")
+ if err != nil {
+ return err
+ }
+
+ err = appendWithWrap(" " + rule)
+ if err != nil {
+ return err
+ }
+
+ for _, dep := range explicitDeps {
+ err := appendWithWrap(" " + dep)
+ if err != nil {
+ return err
+ }
+ }
+
+ if len(implicitDeps) > 0 {
+ err := appendWithWrap(" |")
+ if err != nil {
+ return err
+ }
+
+ for _, dep := range implicitDeps {
+ err := appendWithWrap(" " + dep)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ if len(orderOnlyDeps) > 0 {
+ err := appendWithWrap(" ||")
+ if err != nil {
+ return err
+ }
+
+ for _, dep := range orderOnlyDeps {
+ err := appendWithWrap(" " + dep)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ _, err = fmt.Fprintln(n.writer, line)
+ return err
+}
+
+func (n *ninjaWriter) Assign(name, value string) error {
+ n.justDidBlankLine = false
+ _, err := fmt.Fprintf(n.writer, "%s = %s\n", name, value)
+ return err
+}
+
+func (n *ninjaWriter) ScopedAssign(name, value string) error {
+ n.justDidBlankLine = false
+ indent := strings.Repeat(" ", indentWidth)
+ _, err := fmt.Fprintf(n.writer, "%s%s = %s\n", indent, name, value)
+ return err
+}
+
+func (n *ninjaWriter) Default(targets ...string) error {
+ n.justDidBlankLine = false
+
+ const lineWrapLen = len(" $")
+ const maxLineLen = lineWidth - lineWrapLen
+
+ line := "default"
+
+ appendWithWrap := func(s string) (err error) {
+ if len(line)+len(s) > maxLineLen {
+ _, err = fmt.Fprintf(n.writer, "%s $\n", line)
+ line = strings.Repeat(" ", indentWidth*2)
+ s = strings.TrimLeftFunc(s, unicode.IsSpace)
+ }
+ line += s
+ return
+ }
+
+ for _, target := range targets {
+ err := appendWithWrap(" " + target)
+ if err != nil {
+ return err
+ }
+ }
+
+ _, err := fmt.Fprintln(n.writer, line)
+ return err
+}
+
+func (n *ninjaWriter) BlankLine() (err error) {
+ // We don't output multiple blank lines in a row.
+ if !n.justDidBlankLine {
+ n.justDidBlankLine = true
+ _, err = io.WriteString(n.writer, "\n")
+ }
+ return err
+}
+
+func writeAssignments(w io.Writer, indent int, assignments ...string) error {
+ var maxNameLen int
+ for i := 0; i < len(assignments); i += 2 {
+ name := assignments[i]
+ err := validateNinjaName(name)
+ if err != nil {
+ return err
+ }
+ if maxNameLen < len(name) {
+ maxNameLen = len(name)
+ }
+ }
+
+ indentStr := strings.Repeat(" ", indent*indentWidth)
+ extraIndentStr := strings.Repeat(" ", (indent+1)*indentWidth)
+ replacer := strings.NewReplacer("\n", "$\n"+extraIndentStr)
+
+ for i := 0; i < len(assignments); i += 2 {
+ name := assignments[i]
+ value := replacer.Replace(assignments[i+1])
+ _, err := fmt.Fprintf(w, "%s% *s = %s\n", indentStr, maxNameLen, name,
+ value)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/blueprint/ninja_writer_test.go b/blueprint/ninja_writer_test.go
new file mode 100644
index 0000000..24491e2
--- /dev/null
+++ b/blueprint/ninja_writer_test.go
@@ -0,0 +1,105 @@
+package blueprint
+
+import (
+ "bytes"
+ "testing"
+)
+
+func ck(err error) {
+ if err != nil {
+ panic(err)
+ }
+}
+
+var ninjaWriterTestCases = []struct {
+ input func(w *ninjaWriter)
+ output string
+}{
+ {
+ input: func(w *ninjaWriter) {
+ ck(w.Comment("foo"))
+ },
+ output: "# foo\n",
+ },
+ {
+ input: func(w *ninjaWriter) {
+ ck(w.Pool("foo"))
+ },
+ output: "pool foo\n",
+ },
+ {
+ input: func(w *ninjaWriter) {
+ ck(w.Rule("foo"))
+ },
+ output: "rule foo\n",
+ },
+ {
+ input: func(w *ninjaWriter) {
+ ck(w.Build("foo", []string{"o1", "o2"}, []string{"e1", "e2"},
+ []string{"i1", "i2"}, []string{"oo1", "oo2"}))
+ },
+ output: "build o1 o2: foo e1 e2 | i1 i2 || oo1 oo2\n",
+ },
+ {
+ input: func(w *ninjaWriter) {
+ ck(w.Default("foo"))
+ },
+ output: "default foo\n",
+ },
+ {
+ input: func(w *ninjaWriter) {
+ ck(w.Assign("foo", "bar"))
+ },
+ output: "foo = bar\n",
+ },
+ {
+ input: func(w *ninjaWriter) {
+ ck(w.ScopedAssign("foo", "bar"))
+ },
+ output: " foo = bar\n",
+ },
+ {
+ input: func(w *ninjaWriter) {
+ ck(w.BlankLine())
+ },
+ output: "\n",
+ },
+ {
+ input: func(w *ninjaWriter) {
+ ck(w.Pool("p"))
+ ck(w.ScopedAssign("depth", "3"))
+ ck(w.BlankLine())
+ ck(w.Comment("here comes a rule"))
+ ck(w.Rule("r"))
+ ck(w.ScopedAssign("command", "echo out: $out in: $in _arg: $_arg"))
+ ck(w.ScopedAssign("pool", "p"))
+ ck(w.BlankLine())
+ ck(w.Build("r", []string{"foo.o"}, []string{"foo.in"}, nil, nil))
+ ck(w.ScopedAssign("_arg", "arg value"))
+ },
+ output: `pool p
+ depth = 3
+
+# here comes a rule
+rule r
+ command = echo out: $out in: $in _arg: $_arg
+ pool = p
+
+build foo.o: r foo.in
+ _arg = arg value
+`,
+ },
+}
+
+func TestNinjaWriter(t *testing.T) {
+ for i, testCase := range ninjaWriterTestCases {
+ buf := bytes.NewBuffer(nil)
+ w := newNinjaWriter(buf)
+ testCase.input(w)
+ if buf.String() != testCase.output {
+ t.Errorf("incorrect output for test case %d", i)
+ t.Errorf(" expected: %q", testCase.output)
+ t.Errorf(" got: %q", buf.String())
+ }
+ }
+}
diff --git a/blueprint/parser/parser.go b/blueprint/parser/parser.go
new file mode 100644
index 0000000..626493a
--- /dev/null
+++ b/blueprint/parser/parser.go
@@ -0,0 +1,354 @@
+package parser
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+ "text/scanner"
+)
+
+var errTooManyErrors = errors.New("too many errors")
+
+const maxErrors = 1
+
+type ParseError struct {
+ Err error
+ Pos scanner.Position
+}
+
+func (e *ParseError) Error() string {
+ return fmt.Sprintf("%s: %s", e.Pos, e.Err)
+}
+
+func Parse(filename string, r io.Reader) (defs []Definition, errs []error) {
+ p := newParser(r)
+ p.scanner.Filename = filename
+
+ defer func() {
+ if r := recover(); r != nil {
+ if r == errTooManyErrors {
+ defs = nil
+ errs = p.errors
+ return
+ }
+ panic(r)
+ }
+ }()
+
+ defs = p.parseDefinitions()
+ p.accept(scanner.EOF)
+ errs = p.errors
+
+ return
+}
+
+type parser struct {
+ scanner scanner.Scanner
+ tok rune
+ errors []error
+}
+
+func newParser(r io.Reader) *parser {
+ p := &parser{}
+ p.scanner.Init(r)
+ p.scanner.Error = func(sc *scanner.Scanner, msg string) {
+ p.errorf(msg)
+ }
+ p.scanner.Mode = scanner.ScanIdents | scanner.ScanStrings |
+ scanner.ScanComments | scanner.SkipComments
+ p.next()
+ return p
+}
+
+func (p *parser) errorf(format string, args ...interface{}) {
+ pos := p.scanner.Position
+ if !pos.IsValid() {
+ pos = p.scanner.Pos()
+ }
+ err := &ParseError{
+ Err: fmt.Errorf(format, args...),
+ Pos: pos,
+ }
+ p.errors = append(p.errors, err)
+ if len(p.errors) >= maxErrors {
+ panic(errTooManyErrors)
+ }
+}
+
+func (p *parser) accept(toks ...rune) bool {
+ for _, tok := range toks {
+ if p.tok != tok {
+ p.errorf("expected %s, found %s", scanner.TokenString(tok),
+ scanner.TokenString(p.tok))
+ return false
+ }
+ if p.tok != scanner.EOF {
+ p.tok = p.scanner.Scan()
+ }
+ }
+ return true
+}
+
+func (p *parser) next() {
+ if p.tok != scanner.EOF {
+ p.tok = p.scanner.Scan()
+ }
+ return
+}
+
+func (p *parser) parseDefinitions() (defs []Definition) {
+ for {
+ switch p.tok {
+ case scanner.Ident:
+ ident := p.scanner.TokenText()
+ pos := p.scanner.Position
+
+ p.accept(scanner.Ident)
+
+ switch p.tok {
+ case '=':
+ defs = append(defs, p.parseAssignment(ident, pos))
+ case '{':
+ defs = append(defs, p.parseModule(ident, pos))
+ default:
+ p.errorf("expected \"=\" or \"{\", found %s",
+ scanner.TokenString(p.tok))
+ }
+ case scanner.EOF:
+ return
+ default:
+ p.errorf("expected assignment or module definition, found %s",
+ scanner.TokenString(p.tok))
+ return
+ }
+ }
+}
+
+func (p *parser) parseAssignment(name string,
+ pos scanner.Position) (assignment *Assignment) {
+
+ assignment = new(Assignment)
+
+ if !p.accept('=') {
+ return
+ }
+ value := p.parseValue()
+
+ assignment.Name = name
+ assignment.Value = value
+ assignment.Pos = pos
+
+ return
+}
+
+func (p *parser) parseModule(typ string,
+ pos scanner.Position) (module *Module) {
+
+ module = new(Module)
+
+ if !p.accept('{') {
+ return
+ }
+ properties := p.parsePropertyList()
+ p.accept('}')
+
+ module.Type = typ
+ module.Properties = properties
+ module.Pos = pos
+ return
+}
+
+func (p *parser) parsePropertyList() (properties []*Property) {
+ for p.tok == scanner.Ident {
+ property := p.parseProperty()
+ properties = append(properties, property)
+
+ if p.tok != ',' {
+ // There was no comma, so the list is done.
+ break
+ }
+
+ p.accept(',')
+ }
+
+ return
+}
+
+func (p *parser) parseProperty() (property *Property) {
+ property = new(Property)
+
+ name := p.scanner.TokenText()
+ pos := p.scanner.Position
+ if !p.accept(scanner.Ident, ':') {
+ return
+ }
+ value := p.parseValue()
+
+ property.Name = name
+ property.Value = value
+ property.Pos = pos
+
+ return
+}
+
+func (p *parser) parseValue() (value Value) {
+ switch p.tok {
+ case scanner.Ident:
+ return p.parseBoolValue()
+ case scanner.String:
+ return p.parseStringValue()
+ case '[':
+ return p.parseListValue()
+ default:
+ p.errorf("expected bool, list, or string value; found %s",
+ scanner.TokenString(p.tok))
+ return
+ }
+}
+
+func (p *parser) parseBoolValue() (value Value) {
+ value.Type = Bool
+ value.Pos = p.scanner.Position
+ switch text := p.scanner.TokenText(); text {
+ case "true":
+ value.BoolValue = true
+ case "false":
+ value.BoolValue = false
+ default:
+ p.errorf("expected true or false; found %q", text)
+ return
+ }
+ p.accept(scanner.Ident)
+ return
+}
+
+func (p *parser) parseStringValue() (value Value) {
+ value.Type = String
+ value.Pos = p.scanner.Position
+ str, err := strconv.Unquote(p.scanner.TokenText())
+ if err != nil {
+ p.errorf("couldn't parse string: %s", err)
+ return
+ }
+ value.StringValue = str
+ p.accept(scanner.String)
+ return
+}
+
+func (p *parser) parseListValue() (value Value) {
+ value.Type = List
+ value.Pos = p.scanner.Position
+ if !p.accept('[') {
+ return
+ }
+
+ var elements []Value
+ for p.tok == scanner.String {
+ elements = append(elements, p.parseStringValue())
+
+ if p.tok != ',' {
+ // There was no comma, so the list is done.
+ break
+ }
+
+ p.accept(',')
+ }
+
+ value.ListValue = elements
+
+ p.accept(']')
+ return
+}
+
+type ValueType int
+
+const (
+ Bool ValueType = iota
+ String
+ List
+)
+
+func (p ValueType) String() string {
+ switch p {
+ case Bool:
+ return "bool"
+ case String:
+ return "string"
+ case List:
+ return "list"
+ default:
+ panic(fmt.Errorf("unknown value type: %d", p))
+ }
+}
+
+type Definition interface {
+ String() string
+ definitionTag()
+}
+
+type Assignment struct {
+ Name string
+ Value Value
+ Pos scanner.Position
+}
+
+func (a *Assignment) String() string {
+ return fmt.Sprintf("%s@%d:%s: %s", a.Name, a.Pos.Offset, a.Pos, a.Value)
+}
+
+func (a *Assignment) definitionTag() {}
+
+type Module struct {
+ Type string
+ Properties []*Property
+ Pos scanner.Position
+}
+
+func (m *Module) String() string {
+ propertyStrings := make([]string, len(m.Properties))
+ for i, property := range m.Properties {
+ propertyStrings[i] = property.String()
+ }
+ return fmt.Sprintf("%s@%d:%s{%s}", m.Type, m.Pos.Offset, m.Pos,
+ strings.Join(propertyStrings, ", "))
+}
+
+func (m *Module) definitionTag() {}
+
+type Property struct {
+ Name string
+ Value Value
+ Pos scanner.Position
+}
+
+func (p *Property) String() string {
+ return fmt.Sprintf("%s@%d:%s: %s", p.Name, p.Pos.Offset, p.Pos, p.Value)
+}
+
+type Value struct {
+ Type ValueType
+ BoolValue bool
+ StringValue string
+ ListValue []Value
+ Pos scanner.Position
+}
+
+func (p Value) String() string {
+ switch p.Type {
+ case Bool:
+ return fmt.Sprintf("%t@%d:%s", p.BoolValue, p.Pos.Offset, p.Pos)
+ case String:
+ return fmt.Sprintf("%q@%d:%s", p.StringValue, p.Pos.Offset, p.Pos)
+ case List:
+ valueStrings := make([]string, len(p.ListValue))
+ for i, value := range p.ListValue {
+ valueStrings[i] = value.String()
+ }
+ return fmt.Sprintf("@%d:%s[%s]", p.Pos.Offset, p.Pos,
+ strings.Join(valueStrings, ", "))
+ default:
+ panic(fmt.Errorf("bad property type: %d", p.Type))
+ }
+}
diff --git a/blueprint/parser/parser_test.go b/blueprint/parser/parser_test.go
new file mode 100644
index 0000000..57a1966
--- /dev/null
+++ b/blueprint/parser/parser_test.go
@@ -0,0 +1,234 @@
+package parser
+
+import (
+ "bytes"
+ "reflect"
+ "strings"
+ "testing"
+ "text/scanner"
+)
+
+func mkpos(offset, line, column int) scanner.Position {
+ return scanner.Position{
+ Offset: offset,
+ Line: line,
+ Column: column,
+ }
+}
+
+var validParseTestCases = []struct {
+ input string
+ output []Definition
+}{
+ {`
+ foo {}
+ `,
+ []Definition{
+ &Module{
+ Type: "foo",
+ Pos: mkpos(3, 2, 3),
+ },
+ },
+ },
+
+ {`
+ foo {
+ name: "abc",
+ }
+ `,
+ []Definition{
+ &Module{
+ Type: "foo",
+ Pos: mkpos(3, 2, 3),
+ Properties: []*Property{
+ {
+ Name: "name",
+ Pos: mkpos(12, 3, 4),
+ Value: Value{
+ Type: String,
+ Pos: mkpos(18, 3, 10),
+ StringValue: "abc",
+ },
+ },
+ },
+ },
+ },
+ },
+
+ {`
+ foo {
+ isGood: true,
+ }
+ `,
+ []Definition{
+ &Module{
+ Type: "foo",
+ Pos: mkpos(3, 2, 3),
+ Properties: []*Property{
+ {
+ Name: "isGood",
+ Pos: mkpos(12, 3, 4),
+ Value: Value{
+ Type: Bool,
+ Pos: mkpos(20, 3, 12),
+ BoolValue: true,
+ },
+ },
+ },
+ },
+ },
+ },
+
+ {`
+ foo {
+ stuff: ["asdf", "jkl;", "qwert",
+ "uiop", "bnm,"]
+ }
+ `,
+ []Definition{
+ &Module{
+ Type: "foo",
+ Pos: mkpos(3, 2, 3),
+ Properties: []*Property{
+ {
+ Name: "stuff",
+ Pos: mkpos(12, 3, 4),
+ Value: Value{
+ Type: List,
+ Pos: mkpos(19, 3, 11),
+ ListValue: []Value{
+ Value{
+ Type: String,
+ Pos: mkpos(20, 3, 12),
+ StringValue: "asdf",
+ },
+ Value{
+ Type: String,
+ Pos: mkpos(28, 3, 20),
+ StringValue: "jkl;",
+ },
+ Value{
+ Type: String,
+ Pos: mkpos(36, 3, 28),
+ StringValue: "qwert",
+ },
+ Value{
+ Type: String,
+ Pos: mkpos(49, 4, 5),
+ StringValue: "uiop",
+ },
+ Value{
+ Type: String,
+ Pos: mkpos(57, 4, 13),
+ StringValue: "bnm,",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+
+ {`
+ // comment
+ foo {
+ // comment
+ isGood: true, // comment
+ }
+ `,
+ []Definition{
+ &Module{
+ Type: "foo",
+ Pos: mkpos(16, 3, 3),
+ Properties: []*Property{
+ {
+ Name: "isGood",
+ Pos: mkpos(39, 5, 4),
+ Value: Value{
+ Type: Bool,
+ Pos: mkpos(47, 5, 12),
+ BoolValue: true,
+ },
+ },
+ },
+ },
+ },
+ },
+
+ {`
+ foo {
+ name: "abc",
+ }
+
+ bar {
+ name: "def",
+ }
+ `,
+ []Definition{
+ &Module{
+ Type: "foo",
+ Pos: mkpos(3, 2, 3),
+ Properties: []*Property{
+ {
+ Name: "name",
+ Pos: mkpos(12, 3, 4),
+ Value: Value{
+ Type: String,
+ Pos: mkpos(18, 3, 10),
+ StringValue: "abc",
+ },
+ },
+ },
+ },
+ &Module{
+ Type: "bar",
+ Pos: mkpos(32, 6, 3),
+ Properties: []*Property{
+ {
+ Name: "name",
+ Pos: mkpos(41, 7, 4),
+ Value: Value{
+ Type: String,
+ Pos: mkpos(47, 7, 10),
+ StringValue: "def",
+ },
+ },
+ },
+ },
+ },
+ },
+}
+
+func defListString(defs []Definition) string {
+ defStrings := make([]string, len(defs))
+ for i, def := range defs {
+ defStrings[i] = def.String()
+ }
+
+ return strings.Join(defStrings, ", ")
+}
+
+func TestParseValidInput(t *testing.T) {
+ for _, testCase := range validParseTestCases {
+ r := bytes.NewBufferString(testCase.input)
+ defs, errs := Parse("", r)
+ if len(errs) != 0 {
+ t.Errorf("test case: %s", testCase.input)
+ t.Errorf("unexpected errors:")
+ for _, err := range errs {
+ t.Errorf(" %s", err)
+ }
+ t.FailNow()
+ }
+
+ if !reflect.DeepEqual(defs, testCase.output) {
+ t.Errorf("test case: %s", testCase.input)
+ t.Errorf("incorrect output:")
+ t.Errorf(" expected: %s", defListString(testCase.output))
+ t.Errorf(" got: %s", defListString(defs))
+ }
+ }
+}
+
+// TODO: Test error strings
diff --git a/blueprint/scope.go b/blueprint/scope.go
new file mode 100644
index 0000000..acb4598
--- /dev/null
+++ b/blueprint/scope.go
@@ -0,0 +1,320 @@
+package blueprint
+
+import (
+ "fmt"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+)
+
+// A Variable represents a global Ninja variable definition that will be written
+// to the output .ninja file. A variable may contain references to other global
+// Ninja variables, but circular variable references are not allowed.
+type Variable interface {
+ pkg() *pkg
+ name() string // "foo"
+ fullName(pkgNames map[*pkg]string) string // "pkg.foo" or "path/to/pkg.foo"
+ value(config Config) (*ninjaString, error)
+}
+
+// A Pool represents a Ninja pool that will be written to the output .ninja
+// file.
+type Pool interface {
+ pkg() *pkg
+ name() string // "foo"
+ fullName(pkgNames map[*pkg]string) string // "pkg.foo" or "path/to/pkg.foo"
+ def(config Config) (*poolDef, error)
+}
+
+// A Rule represents a Ninja build rule that will be written to the output
+// .ninja file.
+type Rule interface {
+ pkg() *pkg
+ name() string // "foo"
+ fullName(pkgNames map[*pkg]string) string // "pkg.foo" or "path/to/pkg.foo"
+ def(config Config) (*ruleDef, error)
+ scope() *scope
+ isArg(argName string) bool
+}
+
+type scope struct {
+ parent *scope
+ variables map[string]Variable
+ pools map[string]Pool
+ rules map[string]Rule
+ imports map[string]*scope
+}
+
+func newScope(parent *scope) *scope {
+ return &scope{
+ parent: parent,
+ variables: make(map[string]Variable),
+ pools: make(map[string]Pool),
+ rules: make(map[string]Rule),
+ imports: make(map[string]*scope),
+ }
+}
+
+func makeRuleScope(parent *scope, argNames map[string]bool) *scope {
+ scope := newScope(parent)
+ for argName := range argNames {
+ _, err := scope.LookupVariable(argName)
+ if err != nil {
+ arg := &argVariable{argName}
+ err = scope.AddVariable(arg)
+ if err != nil {
+ // This should not happen. We should have already checked that
+ // the name is valid and that the scope doesn't have a variable
+ // with this name.
+ panic(err)
+ }
+ }
+ }
+
+ // We treat built-in variables like arguments for the purpose of this scope.
+ for _, builtin := range builtinRuleArgs {
+ arg := &argVariable{builtin}
+ err := scope.AddVariable(arg)
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ return scope
+}
+
+func (s *scope) LookupVariable(name string) (Variable, error) {
+ dotIndex := strings.IndexRune(name, '.')
+ if dotIndex >= 0 {
+ // The variable name looks like "pkg.var"
+ if dotIndex+1 == len(name) {
+ return nil, fmt.Errorf("variable name %q ends with a '.'", name)
+ }
+ if strings.ContainsRune(name[dotIndex+1:], '.') {
+ return nil, fmt.Errorf("variable name %q contains multiple '.' "+
+ "characters", name)
+ }
+
+ pkgName := name[:dotIndex]
+ varName := name[dotIndex+1:]
+
+ first, _ := utf8.DecodeRuneInString(varName)
+ if !unicode.IsUpper(first) {
+ return nil, fmt.Errorf("cannot refer to unexported name %q", name)
+ }
+
+ importedScope, err := s.lookupImportedScope(pkgName)
+ if err != nil {
+ return nil, err
+ }
+
+ v, ok := importedScope.variables[varName]
+ if !ok {
+ return nil, fmt.Errorf("package %q does not contain variable %q",
+ pkgName, varName)
+ }
+
+ return v, nil
+ } else {
+ // The variable name has no package part; just "var"
+ for ; s != nil; s = s.parent {
+ v, ok := s.variables[name]
+ if ok {
+ return v, nil
+ }
+ }
+ return nil, fmt.Errorf("undefined variable %q", name)
+ }
+}
+
+func (s *scope) lookupImportedScope(pkgName string) (*scope, error) {
+ for ; s != nil; s = s.parent {
+ importedScope, ok := s.imports[pkgName]
+ if ok {
+ return importedScope, nil
+ }
+ }
+ return nil, fmt.Errorf("unknown imported package %q (missing call to "+
+ "blueprint.Import()?)", pkgName)
+}
+
+func (s *scope) AddImport(name string, importedScope *scope) error {
+ _, present := s.imports[name]
+ if present {
+ return fmt.Errorf("import %q is already defined in this scope", name)
+ }
+ s.imports[name] = importedScope
+ return nil
+}
+
+func (s *scope) AddVariable(v Variable) error {
+ name := v.name()
+ _, present := s.variables[name]
+ if present {
+ return fmt.Errorf("variable %q is already defined in this scope", name)
+ }
+ s.variables[name] = v
+ return nil
+}
+
+func (s *scope) AddPool(p Pool) error {
+ name := p.name()
+ _, present := s.pools[name]
+ if present {
+ return fmt.Errorf("pool %q is already defined in this scope", name)
+ }
+ s.pools[name] = p
+ return nil
+}
+
+func (s *scope) AddRule(r Rule) error {
+ name := r.name()
+ _, present := s.rules[name]
+ if present {
+ return fmt.Errorf("rule %q is already defined in this scope", name)
+ }
+ s.rules[name] = r
+ return nil
+}
+
+type localScope struct {
+ namePrefix string
+ scope *scope
+}
+
+func newLocalScope(parent *scope, namePrefix string) *localScope {
+ return &localScope{
+ namePrefix: namePrefix,
+ scope: newScope(parent),
+ }
+}
+
+func (s *localScope) LookupVariable(name string) (Variable, error) {
+ return s.scope.LookupVariable(name)
+}
+
+func (s *localScope) AddLocalVariable(name, value string) (*localVariable,
+ error) {
+
+ err := validateNinjaName(name)
+ if err != nil {
+ return nil, err
+ }
+
+ if strings.ContainsRune(name, '.') {
+ return nil, fmt.Errorf("local variable name %q contains '.'", name)
+ }
+
+ ninjaValue, err := parseNinjaString(s.scope, value)
+ if err != nil {
+ return nil, err
+ }
+
+ v := &localVariable{
+ namePrefix: s.namePrefix,
+ name_: name,
+ value_: ninjaValue,
+ }
+
+ err = s.scope.AddVariable(v)
+ if err != nil {
+ return nil, err
+ }
+
+ return v, nil
+}
+
+func (s *localScope) AddLocalRule(name string, params *RuleParams,
+ argNames ...string) (*localRule, error) {
+
+ err := validateNinjaName(name)
+ if err != nil {
+ return nil, err
+ }
+
+ err = validateArgNames(argNames)
+ if err != nil {
+ return nil, fmt.Errorf("invalid argument name: %s", err)
+ }
+
+ argNamesSet := make(map[string]bool)
+ for _, argName := range argNames {
+ argNamesSet[argName] = true
+ }
+
+ ruleScope := makeRuleScope(s.scope, argNamesSet)
+
+ def, err := parseRuleParams(ruleScope, params)
+ if err != nil {
+ return nil, err
+ }
+
+ r := &localRule{
+ namePrefix: s.namePrefix,
+ name_: name,
+ def_: def,
+ argNames: argNamesSet,
+ scope_: ruleScope,
+ }
+
+ err = s.scope.AddRule(r)
+ if err != nil {
+ return nil, err
+ }
+
+ return r, nil
+}
+
+type localVariable struct {
+ namePrefix string
+ name_ string
+ value_ *ninjaString
+}
+
+func (l *localVariable) pkg() *pkg {
+ return nil
+}
+
+func (l *localVariable) name() string {
+ return l.name_
+}
+
+func (l *localVariable) fullName(pkgNames map[*pkg]string) string {
+ return l.namePrefix + l.name_
+}
+
+func (l *localVariable) value(Config) (*ninjaString, error) {
+ return l.value_, nil
+}
+
+type localRule struct {
+ namePrefix string
+ name_ string
+ def_ *ruleDef
+ argNames map[string]bool
+ scope_ *scope
+}
+
+func (l *localRule) pkg() *pkg {
+ return nil
+}
+
+func (l *localRule) name() string {
+ return l.name_
+}
+
+func (l *localRule) fullName(pkgNames map[*pkg]string) string {
+ return l.namePrefix + l.name_
+}
+
+func (l *localRule) def(Config) (*ruleDef, error) {
+ return l.def_, nil
+}
+
+func (r *localRule) scope() *scope {
+ return r.scope_
+}
+
+func (r *localRule) isArg(argName string) bool {
+ return r.argNames[argName]
+}
diff --git a/blueprint/singleton_ctx.go b/blueprint/singleton_ctx.go
new file mode 100644
index 0000000..549d079
--- /dev/null
+++ b/blueprint/singleton_ctx.go
@@ -0,0 +1,137 @@
+package blueprint
+
+import (
+ "fmt"
+ "path/filepath"
+)
+
+type Singleton interface {
+ GenerateBuildActions(SingletonContext)
+}
+
+type SingletonContext interface {
+ Config() Config
+
+ ModuleName(module Module) string
+ ModuleDir(module Module) string
+ BlueprintFile(module Module) string
+
+ ModuleErrorf(module Module, format string, args ...interface{})
+ Errorf(format string, args ...interface{})
+
+ Variable(name, value string)
+ Rule(name string, params RuleParams) Rule
+ Build(params BuildParams)
+ RequireNinjaVersion(major, minor, micro int)
+
+ // SetBuildDir sets the value of the top-level "builddir" Ninja variable
+ // that controls where Ninja stores its build log files. This value can be
+ // set at most one time for a single build. Setting it multiple times (even
+ // across different singletons) will result in a panic.
+ SetBuildDir(value string)
+
+ VisitAllModules(visit func(Module))
+ VisitAllModulesIf(pred func(Module) bool, visit func(Module))
+}
+
+var _ SingletonContext = (*singletonContext)(nil)
+
+type singletonContext struct {
+ context *Context
+ config Config
+ scope *localScope
+
+ errs []error
+
+ actionDefs localBuildActions
+}
+
+func (s *singletonContext) Config() Config {
+ return s.config
+}
+
+func (s *singletonContext) ModuleName(module Module) string {
+ info := s.context.moduleInfo[module]
+ return info.properties.Name
+}
+
+func (s *singletonContext) ModuleDir(module Module) string {
+ info := s.context.moduleInfo[module]
+ return filepath.Dir(info.relBlueprintFile)
+}
+
+func (s *singletonContext) BlueprintFile(module Module) string {
+ info := s.context.moduleInfo[module]
+ return info.relBlueprintFile
+}
+
+func (s *singletonContext) ModuleErrorf(module Module, format string,
+ args ...interface{}) {
+
+ info := s.context.moduleInfo[module]
+ s.errs = append(s.errs, &Error{
+ Err: fmt.Errorf(format, args...),
+ Pos: info.pos,
+ })
+}
+
+func (s *singletonContext) Errorf(format string, args ...interface{}) {
+ // TODO: Make this not result in the error being printed as "internal error"
+ s.errs = append(s.errs, fmt.Errorf(format, args...))
+}
+
+func (s *singletonContext) Variable(name, value string) {
+ v, err := s.scope.AddLocalVariable(name, value)
+ if err != nil {
+ panic(err)
+ }
+
+ s.actionDefs.variables = append(s.actionDefs.variables, v)
+}
+
+func (s *singletonContext) Rule(name string, params RuleParams) Rule {
+ // TODO: Verify that params.Pool is accessible in this module's scope.
+
+ r, err := s.scope.AddLocalRule(name, ¶ms)
+ if err != nil {
+ panic(err)
+ }
+
+ s.actionDefs.rules = append(s.actionDefs.rules, r)
+
+ return r
+}
+
+func (s *singletonContext) Build(params BuildParams) {
+ // TODO: Verify that params.Rule is accessible in this module's scope.
+
+ def, err := parseBuildParams(s.scope, ¶ms)
+ if err != nil {
+ panic(err)
+ }
+
+ s.actionDefs.buildDefs = append(s.actionDefs.buildDefs, def)
+}
+
+func (s *singletonContext) RequireNinjaVersion(major, minor, micro int) {
+ s.context.requireNinjaVersion(major, minor, micro)
+}
+
+func (s *singletonContext) SetBuildDir(value string) {
+ ninjaValue, err := parseNinjaString(s.scope, value)
+ if err != nil {
+ panic(err)
+ }
+
+ s.context.setBuildDir(ninjaValue)
+}
+
+func (s *singletonContext) VisitAllModules(visit func(Module)) {
+ s.context.visitAllModules(visit)
+}
+
+func (s *singletonContext) VisitAllModulesIf(pred func(Module) bool,
+ visit func(Module)) {
+
+ s.context.visitAllModulesIf(pred, visit)
+}
diff --git a/blueprint/unpack.go b/blueprint/unpack.go
new file mode 100644
index 0000000..98f0fe2
--- /dev/null
+++ b/blueprint/unpack.go
@@ -0,0 +1,196 @@
+package blueprint
+
+import (
+ "blueprint/parser"
+ "fmt"
+ "reflect"
+ "unicode"
+ "unicode/utf8"
+)
+
+type packedProperty struct {
+ property *parser.Property
+ unpacked bool
+}
+
+func unpackProperties(propertyDefs []*parser.Property,
+ propertiesStructs ...interface{}) (errs []error) {
+
+ propertyMap := make(map[string]*packedProperty)
+ for _, propertyDef := range propertyDefs {
+ name := propertyDef.Name
+ if first, present := propertyMap[name]; present {
+ errs = append(errs, &Error{
+ Err: fmt.Errorf("property %q already defined", name),
+ Pos: propertyDef.Pos,
+ })
+ errs = append(errs, &Error{
+ Err: fmt.Errorf("--> previous definition here"),
+ Pos: first.property.Pos,
+ })
+ if len(errs) >= maxErrors {
+ return errs
+ }
+ continue
+ }
+
+ propertyMap[name] = &packedProperty{
+ property: propertyDef,
+ unpacked: false,
+ }
+ }
+
+ for _, properties := range propertiesStructs {
+ propertiesValue := reflect.ValueOf(properties)
+ if propertiesValue.Kind() != reflect.Ptr {
+ panic("properties must be a pointer to a struct")
+ }
+
+ propertiesValue = propertiesValue.Elem()
+ if propertiesValue.Kind() != reflect.Struct {
+ panic("properties must be a pointer to a struct")
+ }
+
+ newErrs := unpackStruct(propertiesValue, propertyMap)
+ errs = append(errs, newErrs...)
+
+ if len(errs) >= maxErrors {
+ return errs
+ }
+ }
+
+ // Report any properties that didn't have corresponding struct fields as
+ // errors.
+ for name, packedProperty := range propertyMap {
+ if !packedProperty.unpacked {
+ err := &Error{
+ Err: fmt.Errorf("unrecognized property %q", name),
+ Pos: packedProperty.property.Pos,
+ }
+ errs = append(errs, err)
+ }
+ }
+
+ return errs
+}
+
+func unpackStruct(structValue reflect.Value,
+ propertyMap map[string]*packedProperty) []error {
+
+ structType := structValue.Type()
+
+ var errs []error
+ for i := 0; i < structValue.NumField(); i++ {
+ fieldValue := structValue.Field(i)
+ field := structType.Field(i)
+
+ if !fieldValue.CanSet() {
+ panic(fmt.Errorf("field %s is not settable", field.Name))
+ }
+
+ // To make testing easier we validate the struct field's type regardless
+ // of whether or not the property was specified in the parsed string.
+ switch kind := fieldValue.Kind(); kind {
+ case reflect.Bool, reflect.String:
+ // Do nothing
+ case reflect.Slice:
+ elemType := field.Type.Elem()
+ if elemType.Kind() != reflect.String {
+ panic(fmt.Errorf("field %s is a non-string slice", field.Name))
+ }
+ case reflect.Struct:
+ newErrs := unpackStruct(fieldValue, propertyMap)
+ errs = append(errs, newErrs...)
+ if len(errs) >= maxErrors {
+ return errs
+ }
+ continue // This field doesn't correspond to a specific property.
+ default:
+ panic(fmt.Errorf("unsupported kind for field %s: %s",
+ field.Name, kind))
+ }
+
+ // Get the property value if it was specified.
+ propertyName := propertyNameForField(field)
+ packedProperty, ok := propertyMap[propertyName]
+ if !ok {
+ // This property wasn't specified.
+ continue
+ }
+
+ packedProperty.unpacked = true
+
+ var newErrs []error
+ switch kind := fieldValue.Kind(); kind {
+ case reflect.Bool:
+ newErrs = unpackBool(fieldValue, packedProperty.property)
+ case reflect.String:
+ newErrs = unpackString(fieldValue, packedProperty.property)
+ case reflect.Slice:
+ newErrs = unpackSlice(fieldValue, packedProperty.property)
+ }
+ errs = append(errs, newErrs...)
+ if len(errs) >= maxErrors {
+ return errs
+ }
+ }
+
+ return errs
+}
+
+func unpackBool(boolValue reflect.Value, property *parser.Property) []error {
+ if property.Value.Type != parser.Bool {
+ return []error{
+ fmt.Errorf("%s: can't assign %s value to %s property %q",
+ property.Value.Pos, property.Value.Type, parser.Bool,
+ property.Name),
+ }
+ }
+ boolValue.SetBool(property.Value.BoolValue)
+ return nil
+}
+
+func unpackString(stringValue reflect.Value,
+ property *parser.Property) []error {
+
+ if property.Value.Type != parser.String {
+ return []error{
+ fmt.Errorf("%s: can't assign %s value to %s property %q",
+ property.Value.Pos, property.Value.Type, parser.String,
+ property.Name),
+ }
+ }
+ stringValue.SetString(property.Value.StringValue)
+ return nil
+}
+
+func unpackSlice(sliceValue reflect.Value, property *parser.Property) []error {
+ if property.Value.Type != parser.List {
+ return []error{
+ fmt.Errorf("%s: can't assign %s value to %s property %q",
+ property.Value.Pos, property.Value.Type, parser.List,
+ property.Name),
+ }
+ }
+
+ var list []string
+ for _, value := range property.Value.ListValue {
+ if value.Type != parser.String {
+ // The parser should not produce this.
+ panic("non-string value found in list")
+ }
+ list = append(list, value.StringValue)
+ }
+
+ sliceValue.Set(reflect.ValueOf(list))
+ return nil
+}
+
+func propertyNameForField(field reflect.StructField) string {
+ r, size := utf8.DecodeRuneInString(field.Name)
+ propertyName := string(unicode.ToLower(r))
+ if len(field.Name) > size {
+ propertyName += field.Name[size:]
+ }
+ return propertyName
+}
diff --git a/blueprint/unpack_test.go b/blueprint/unpack_test.go
new file mode 100644
index 0000000..91a9fbb
--- /dev/null
+++ b/blueprint/unpack_test.go
@@ -0,0 +1,124 @@
+package blueprint
+
+import (
+ "blueprint/parser"
+ "bytes"
+ "reflect"
+ "testing"
+)
+
+var validUnpackTestCases = []struct {
+ input string
+ output interface{}
+}{
+ {`
+ m {
+ name: "abc",
+ }
+ `,
+ struct {
+ Name string
+ }{
+ Name: "abc",
+ },
+ },
+
+ {`
+ m {
+ isGood: true,
+ }
+ `,
+ struct {
+ IsGood bool
+ }{
+ IsGood: true,
+ },
+ },
+
+ {`
+ m {
+ stuff: ["asdf", "jkl;", "qwert",
+ "uiop", "bnm,"]
+ }
+ `,
+ struct {
+ Stuff []string
+ }{
+ Stuff: []string{"asdf", "jkl;", "qwert", "uiop", "bnm,"},
+ },
+ },
+
+ {`
+ m {
+ name: "abc",
+ }
+ `,
+ struct {
+ Nested struct {
+ Name string
+ }
+ }{
+ Nested: struct{ Name string }{
+ Name: "abc",
+ },
+ },
+ },
+
+ {`
+ m {
+ foo: "abc",
+ bar: false,
+ baz: ["def", "ghi"],
+ }
+ `,
+ struct {
+ Nested struct {
+ Foo string
+ }
+ Bar bool
+ Baz []string
+ }{
+ Nested: struct{ Foo string }{
+ Foo: "abc",
+ },
+ Bar: false,
+ Baz: []string{"def", "ghi"},
+ },
+ },
+}
+
+func TestUnpackProperties(t *testing.T) {
+ for _, testCase := range validUnpackTestCases {
+ r := bytes.NewBufferString(testCase.input)
+ defs, errs := parser.Parse("", r)
+ if len(errs) != 0 {
+ t.Errorf("test case: %s", testCase.input)
+ t.Errorf("unexpected parse errors:")
+ for _, err := range errs {
+ t.Errorf(" %s", err)
+ }
+ t.FailNow()
+ }
+
+ module := defs[0].(*parser.Module)
+ propertiesType := reflect.TypeOf(testCase.output)
+ properties := reflect.New(propertiesType)
+ errs = unpackProperties(module.Properties, properties.Interface())
+ if len(errs) != 0 {
+ t.Errorf("test case: %s", testCase.input)
+ t.Errorf("unexpected unpack errors:")
+ for _, err := range errs {
+ t.Errorf(" %s", err)
+ }
+ t.FailNow()
+ }
+
+ output := properties.Elem().Interface()
+ if !reflect.DeepEqual(output, testCase.output) {
+ t.Errorf("test case: %s", testCase.input)
+ t.Errorf("incorrect output:")
+ t.Errorf(" expected: %+v", testCase.output)
+ t.Errorf(" got: %+v", output)
+ }
+ }
+}
diff --git a/bootstrap.bash b/bootstrap.bash
new file mode 100755
index 0000000..f9ad8ad
--- /dev/null
+++ b/bootstrap.bash
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+# SRCDIR should be set to the path of the root source directory. It can be
+# either an absolute path or a path relative to the build directory. Whether
+# its an absolute or relative path determines whether the build directory can be
+# moved relative to or along with the source directory without re-running the
+# bootstrap script.
+SRCDIR=`dirname "${BASH_SOURCE[0]}"`
+
+# BOOTSTRAP should be set to the path of this script. It can be either an
+# absolute path or one relative to the build directory (which of these is used
+# should probably match what's used for SRCDIR).
+BOOTSTRAP="${BASH_SOURCE[0]}"
+
+# These variables should be set by auto-detecting or knowing a priori the Go
+# toolchain properties.
+GOROOT=`go env GOROOT`
+GOOS=`go env GOHOSTOS`
+GOARCH=`go env GOHOSTARCH`
+GOCHAR=`go env GOCHAR`
+
+case "$#" in
+ 1) IN="$1";BOOTSTRAP_MANIFEST="$1";;
+ 2) IN="$1";BOOTSTRAP_MANIFEST="$2";;
+ *) IN="${SRCDIR}/build.ninja.in";BOOTSTRAP_MANIFEST="$IN";;
+esac
+
+sed -e "s|@@SrcDir@@|$SRCDIR|g" \
+ -e "s|@@GoRoot@@|$GOROOT|g" \
+ -e "s|@@GoOS@@|$GOOS|g" \
+ -e "s|@@GoArch@@|$GOARCH|g" \
+ -e "s|@@GoChar@@|$GOCHAR|g" \
+ -e "s|@@Bootstrap@@|$BOOTSTRAP|g" \
+ -e "s|@@BootstrapManifest@@|$BOOTSTRAP_MANIFEST|g" \
+ $IN > build.ninja
\ No newline at end of file
diff --git a/build.ninja.in b/build.ninja.in
new file mode 100644
index 0000000..b7983ab
--- /dev/null
+++ b/build.ninja.in
@@ -0,0 +1,156 @@
+# ******************************************************************************
+# *** This file is generated and should not be edited ***
+# ******************************************************************************
+#
+# This file contains variables, rules, and pools with name prefixes indicating
+# they were generated by the following Go packages:
+#
+# bootstrap [from Go package blueprint/bootstrap]
+#
+ninja_required_version = 1.1.0
+
+g.bootstrap.Bootstrap = @@Bootstrap@@
+
+g.bootstrap.BootstrapManifest = @@BootstrapManifest@@
+
+g.bootstrap.GoArch = @@GoArch@@
+
+g.bootstrap.GoChar = @@GoChar@@
+
+g.bootstrap.GoOS = @@GoOS@@
+
+g.bootstrap.GoRoot = @@GoRoot@@
+
+g.bootstrap.SrcDir = @@SrcDir@@
+
+g.bootstrap.goToolDir = ${g.bootstrap.GoRoot}/pkg/tool/${g.bootstrap.GoOS}_${g.bootstrap.GoArch}
+
+g.bootstrap.gcCmd = ${g.bootstrap.goToolDir}/${g.bootstrap.GoChar}g
+
+g.bootstrap.linkCmd = ${g.bootstrap.goToolDir}/${g.bootstrap.GoChar}l
+
+g.bootstrap.packCmd = ${g.bootstrap.goToolDir}/pack
+
+builddir = bootstrap
+
+rule g.bootstrap.gc
+ command = GOROOT='${g.bootstrap.GoRoot}' ${g.bootstrap.gcCmd} -o ${out} -p ${pkgPath} -complete ${incFlags} ${in}
+ description = ${g.bootstrap.GoChar}g ${out}
+
+rule g.bootstrap.pack
+ command = GOROOT='${g.bootstrap.GoRoot}' ${g.bootstrap.packCmd} grcP ${prefix} ${out} ${in}
+ description = pack ${out}
+
+rule g.bootstrap.link
+ command = GOROOT='${g.bootstrap.GoRoot}' ${g.bootstrap.linkCmd} -o ${out} ${libDirFlags} ${in}
+ description = ${g.bootstrap.GoChar}l ${out}
+
+rule g.bootstrap.cp
+ command = cp ${in} ${out}
+ description = cp ${out}
+
+rule g.bootstrap.bootstrap
+ command = ${g.bootstrap.Bootstrap} ${in} ${g.bootstrap.BootstrapManifest}
+ description = bootstrap ${in}
+ generator = true
+
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# Module: blueprint
+# Type: bootstrap_go_package
+# GoType: blueprint/bootstrap.goPackageModule
+# Defined: ../Blueprints:1:1
+
+build bootstrap/blueprint/obj/_go_.${g.bootstrap.GoChar}: g.bootstrap.gc $
+ ${g.bootstrap.SrcDir}/blueprint/context.go $
+ ${g.bootstrap.SrcDir}/blueprint/globals.go $
+ ${g.bootstrap.SrcDir}/blueprint/live_tracker.go $
+ ${g.bootstrap.SrcDir}/blueprint/mangle.go $
+ ${g.bootstrap.SrcDir}/blueprint/module_ctx.go $
+ ${g.bootstrap.SrcDir}/blueprint/ninja_defs.go $
+ ${g.bootstrap.SrcDir}/blueprint/ninja_strings.go $
+ ${g.bootstrap.SrcDir}/blueprint/ninja_writer.go $
+ ${g.bootstrap.SrcDir}/blueprint/scope.go $
+ ${g.bootstrap.SrcDir}/blueprint/singleton_ctx.go $
+ ${g.bootstrap.SrcDir}/blueprint/unpack.go | $
+ bootstrap/blueprint-parser/pkg/blueprint/parser.a
+ pkgPath = blueprint
+ incFlags = -I bootstrap/blueprint-parser/pkg
+
+build bootstrap/blueprint/pkg/blueprint.a: g.bootstrap.pack $
+ bootstrap/blueprint/obj/_go_.${g.bootstrap.GoChar}
+ prefix = bootstrap/blueprint/pkg
+
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# Module: blueprint-parser
+# Type: bootstrap_go_package
+# GoType: blueprint/bootstrap.goPackageModule
+# Defined: ../Blueprints:18:1
+
+build bootstrap/blueprint-parser/obj/_go_.${g.bootstrap.GoChar}: $
+ g.bootstrap.gc ${g.bootstrap.SrcDir}/blueprint/parser/parser.go
+ pkgPath = blueprint/parser
+
+build bootstrap/blueprint-parser/pkg/blueprint/parser.a: g.bootstrap.pack $
+ bootstrap/blueprint-parser/obj/_go_.${g.bootstrap.GoChar}
+ prefix = bootstrap/blueprint-parser/pkg
+
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# Module: blueprint-bootstrap
+# Type: bootstrap_go_package
+# GoType: blueprint/bootstrap.goPackageModule
+# Defined: ../Blueprints:24:1
+
+build bootstrap/blueprint-bootstrap/obj/_go_.${g.bootstrap.GoChar}: $
+ g.bootstrap.gc ${g.bootstrap.SrcDir}/blueprint/bootstrap/bootstrap.go $
+ ${g.bootstrap.SrcDir}/blueprint/bootstrap/command.go $
+ ${g.bootstrap.SrcDir}/blueprint/bootstrap/config.go $
+ ${g.bootstrap.SrcDir}/blueprint/bootstrap/doc.go | $
+ bootstrap/blueprint-parser/pkg/blueprint/parser.a $
+ bootstrap/blueprint/pkg/blueprint.a
+ pkgPath = blueprint/bootstrap
+ incFlags = -I bootstrap/blueprint-parser/pkg -I bootstrap/blueprint/pkg
+
+build bootstrap/blueprint-bootstrap/pkg/blueprint/bootstrap.a: $
+ g.bootstrap.pack $
+ bootstrap/blueprint-bootstrap/obj/_go_.${g.bootstrap.GoChar}
+ prefix = bootstrap/blueprint-bootstrap/pkg
+
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# Module: minibp
+# Type: bootstrap_go_binary
+# GoType: blueprint/bootstrap.goBinaryModule
+# Defined: ../Blueprints:34:1
+
+build bootstrap/minibp/obj/_go_.${g.bootstrap.GoChar}: g.bootstrap.gc $
+ ${g.bootstrap.SrcDir}/blueprint/bootstrap/minibp/main.go | $
+ bootstrap/blueprint-parser/pkg/blueprint/parser.a $
+ bootstrap/blueprint/pkg/blueprint.a $
+ bootstrap/blueprint-bootstrap/pkg/blueprint/bootstrap.a
+ pkgPath = minibp
+ incFlags = -I bootstrap/blueprint-parser/pkg -I bootstrap/blueprint/pkg -I bootstrap/blueprint-bootstrap/pkg
+
+build bootstrap/minibp/obj/minibp.a: g.bootstrap.pack $
+ bootstrap/minibp/obj/_go_.${g.bootstrap.GoChar}
+ prefix = bootstrap/minibp/obj
+
+build bootstrap/minibp/obj/a.out: g.bootstrap.link $
+ bootstrap/minibp/obj/minibp.a
+ libDirFlags = -L bootstrap/blueprint-parser/pkg -L bootstrap/blueprint/pkg -L bootstrap/blueprint-bootstrap/pkg
+
+build bootstrap/bin/minibp: g.bootstrap.cp bootstrap/minibp/obj/a.out
+
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# Singleton: bootstrap
+# GoType: blueprint/bootstrap.singleton
+
+rule s.bootstrap.bigbp
+ command = bootstrap/bin/minibp -p -d bootstrap/build.ninja.in.d -o ${out} ${in}
+ depfile = bootstrap/build.ninja.in.d
+ description = minibp ${out}
+
+build bootstrap/build.ninja.in: s.bootstrap.bigbp $
+ ${g.bootstrap.SrcDir}/Blueprints | bootstrap/bin/minibp
+build bootstrap/notAFile: phony
+build build.ninja: g.bootstrap.bootstrap bootstrap/build.ninja.in | $
+ ${g.bootstrap.Bootstrap} bootstrap/notAFile bootstrap/bin/minibp
+