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
This commit is contained in:
parent
2ec08c8555
commit
1bc967ed43
25 changed files with 5629 additions and 0 deletions
38
Blueprints
Normal file
38
Blueprints
Normal file
|
@ -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"],
|
||||||
|
}
|
490
blueprint/bootstrap/bootstrap.go
Normal file
490
blueprint/bootstrap/bootstrap.go
Normal file
|
@ -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
|
||||||
|
}
|
147
blueprint/bootstrap/command.go
Normal file
147
blueprint/bootstrap/command.go
Normal file
|
@ -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
|
||||||
|
}
|
29
blueprint/bootstrap/config.go
Normal file
29
blueprint/bootstrap/config.go
Normal file
|
@ -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
|
||||||
|
}
|
29
blueprint/bootstrap/doc.go
Normal file
29
blueprint/bootstrap/doc.go
Normal file
|
@ -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
|
32
blueprint/bootstrap/minibp/main.go
Normal file
32
blueprint/bootstrap/minibp/main.go
Normal file
|
@ -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)
|
||||||
|
}
|
1363
blueprint/context.go
Normal file
1363
blueprint/context.go
Normal file
File diff suppressed because it is too large
Load diff
91
blueprint/context_test.go
Normal file
91
blueprint/context_test.go
Normal file
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
533
blueprint/globals.go
Normal file
533
blueprint/globals.go
Normal file
|
@ -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_()
|
||||||
|
}
|
149
blueprint/live_tracker.go
Normal file
149
blueprint/live_tracker.go
Normal file
|
@ -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
|
||||||
|
}
|
13
blueprint/mangle.go
Normal file
13
blueprint/mangle.go
Normal file
|
@ -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 + "."
|
||||||
|
}
|
116
blueprint/module_ctx.go
Normal file
116
blueprint/module_ctx.go
Normal file
|
@ -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)
|
||||||
|
}
|
299
blueprint/ninja_defs.go
Normal file
299
blueprint/ninja_defs.go
Normal file
|
@ -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
|
||||||
|
}
|
281
blueprint/ninja_strings.go
Normal file
281
blueprint/ninja_strings.go
Normal file
|
@ -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
|
||||||
|
}
|
114
blueprint/ninja_strings_test.go
Normal file
114
blueprint/ninja_strings_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
244
blueprint/ninja_writer.go
Normal file
244
blueprint/ninja_writer.go
Normal file
|
@ -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
|
||||||
|
}
|
105
blueprint/ninja_writer_test.go
Normal file
105
blueprint/ninja_writer_test.go
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
354
blueprint/parser/parser.go
Normal file
354
blueprint/parser/parser.go
Normal file
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
234
blueprint/parser/parser_test.go
Normal file
234
blueprint/parser/parser_test.go
Normal file
|
@ -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
|
320
blueprint/scope.go
Normal file
320
blueprint/scope.go
Normal file
|
@ -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]
|
||||||
|
}
|
137
blueprint/singleton_ctx.go
Normal file
137
blueprint/singleton_ctx.go
Normal file
|
@ -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)
|
||||||
|
}
|
196
blueprint/unpack.go
Normal file
196
blueprint/unpack.go
Normal file
|
@ -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
|
||||||
|
}
|
124
blueprint/unpack_test.go
Normal file
124
blueprint/unpack_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
bootstrap.bash
Executable file
35
bootstrap.bash
Executable file
|
@ -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
|
156
build.ninja.in
Normal file
156
build.ninja.in
Normal file
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue