777 lines
18 KiB
Go
777 lines
18 KiB
Go
// Copyright 2015 Google Inc. All rights reserved
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package kati
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang/glog"
|
|
)
|
|
|
|
type nodeState int
|
|
|
|
const (
|
|
nodeInit nodeState = iota // not visited
|
|
nodeVisit // visited
|
|
nodeFile // visited & file exists
|
|
nodeAlias // visited & alias for other target
|
|
nodeMissing // visited & no target for this output
|
|
nodeBuild // visited & build emitted
|
|
)
|
|
|
|
func (s nodeState) String() string {
|
|
switch s {
|
|
case nodeInit:
|
|
return "node-init"
|
|
case nodeVisit:
|
|
return "node-visit"
|
|
case nodeFile:
|
|
return "node-file"
|
|
case nodeAlias:
|
|
return "node-alias"
|
|
case nodeMissing:
|
|
return "node-missing"
|
|
case nodeBuild:
|
|
return "node-build"
|
|
default:
|
|
return fmt.Sprintf("node-unknown[%d]", int(s))
|
|
}
|
|
}
|
|
|
|
// NinjaGenerator generates ninja build files from DepGraph.
|
|
type NinjaGenerator struct {
|
|
// Args is original arguments to generate the ninja file.
|
|
Args []string
|
|
// Suffix is suffix for generated files.
|
|
Suffix string
|
|
// GomaDir is goma directory. If empty, goma will not be used.
|
|
GomaDir string
|
|
// DetectAndroidEcho detects echo as description.
|
|
DetectAndroidEcho bool
|
|
|
|
f *os.File
|
|
nodes []*DepNode
|
|
exports map[string]bool
|
|
|
|
ctx *execContext
|
|
|
|
ruleID int
|
|
done map[string]nodeState
|
|
}
|
|
|
|
func (n *NinjaGenerator) init(g *DepGraph) {
|
|
g.resolveVPATH()
|
|
n.nodes = g.nodes
|
|
n.exports = g.exports
|
|
n.ctx = newExecContext(g.vars, g.vpaths, true)
|
|
n.done = make(map[string]nodeState)
|
|
}
|
|
|
|
func getDepfileImpl(ss string) (string, error) {
|
|
tss := ss + " "
|
|
if (!strings.Contains(tss, " -MD ") && !strings.Contains(tss, " -MMD ")) || !strings.Contains(tss, " -c ") {
|
|
return "", nil
|
|
}
|
|
|
|
mfIndex := strings.Index(ss, " -MF ")
|
|
if mfIndex >= 0 {
|
|
mf := trimLeftSpace(ss[mfIndex+4:])
|
|
if strings.Index(mf, " -MF ") >= 0 {
|
|
return "", fmt.Errorf("Multiple output file candidates in %s", ss)
|
|
}
|
|
mfEndIndex := strings.IndexAny(mf, " \t\n")
|
|
if mfEndIndex >= 0 {
|
|
mf = mf[:mfEndIndex]
|
|
}
|
|
|
|
return mf, nil
|
|
}
|
|
|
|
outIndex := strings.Index(ss, " -o ")
|
|
if outIndex < 0 {
|
|
return "", fmt.Errorf("Cannot find the depfile in %s", ss)
|
|
}
|
|
out := trimLeftSpace(ss[outIndex+4:])
|
|
if strings.Index(out, " -o ") >= 0 {
|
|
return "", fmt.Errorf("Multiple output file candidates in %s", ss)
|
|
}
|
|
outEndIndex := strings.IndexAny(out, " \t\n")
|
|
if outEndIndex >= 0 {
|
|
out = out[:outEndIndex]
|
|
}
|
|
return stripExt(out) + ".d", nil
|
|
}
|
|
|
|
// getDepfile gets depfile from cmdline, and returns cmdline and depfile.
|
|
func getDepfile(cmdline string) (string, string, error) {
|
|
// A hack for Android - llvm-rs-cc seems not to emit a dep file.
|
|
if strings.Contains(cmdline, "bin/llvm-rs-cc ") {
|
|
return cmdline, "", nil
|
|
}
|
|
|
|
depfile, err := getDepfileImpl(cmdline)
|
|
if depfile == "" || err != nil {
|
|
return cmdline, depfile, err
|
|
}
|
|
|
|
// A hack for Makefiles generated by automake.
|
|
mvCmd := "(mv -f " + depfile + " "
|
|
if i := strings.LastIndex(cmdline, mvCmd); i >= 0 {
|
|
rest := cmdline[i+len(mvCmd):]
|
|
ei := strings.IndexByte(rest, ')')
|
|
if ei < 0 {
|
|
return cmdline, "", fmt.Errorf("unbalanced parenthes? %s", cmdline)
|
|
}
|
|
cmdline = cmdline[:i] + "(cp -f " + depfile + " " + rest
|
|
return cmdline, depfile, nil
|
|
}
|
|
|
|
// A hack for Android to get .P files instead of .d.
|
|
p := stripExt(depfile) + ".P"
|
|
if strings.Contains(cmdline, p) {
|
|
rmfCmd := "; rm -f " + depfile
|
|
ncmdline := strings.Replace(cmdline, rmfCmd, "", 1)
|
|
if ncmdline == cmdline {
|
|
return cmdline, "", fmt.Errorf("cannot find removal of .d file: %s", cmdline)
|
|
}
|
|
return ncmdline, p, nil
|
|
}
|
|
|
|
// A hack for Android. For .s files, GCC does not use
|
|
// C preprocessor, so it ignores -MF flag.
|
|
as := "/" + stripExt(filepath.Base(depfile)) + ".s"
|
|
if strings.Contains(cmdline, as) {
|
|
return cmdline, "", nil
|
|
}
|
|
|
|
cmdline += fmt.Sprintf(" && cp %s %s.tmp", depfile, depfile)
|
|
depfile += ".tmp"
|
|
return cmdline, depfile, nil
|
|
}
|
|
|
|
func trimTailingSlash(s string) string {
|
|
if s == "" {
|
|
return s
|
|
}
|
|
if s[len(s)-1] != '\\' {
|
|
return s
|
|
}
|
|
// drop single trailing slash - multiline_arg.mk
|
|
if len(s) > 2 && s[len(s)-2] != '\\' {
|
|
return s[:len(s)-1]
|
|
}
|
|
// preserve two trailing slash - escaped_backslash.mk
|
|
return s
|
|
}
|
|
|
|
func stripShellComment(s string) string {
|
|
if strings.IndexByte(s, '#') < 0 {
|
|
// Fast path.
|
|
return s
|
|
}
|
|
// set space as an initial value so the leading comment will be
|
|
// stripped out.
|
|
lastch := rune(' ')
|
|
var escape bool
|
|
var quote rune
|
|
var skip rune
|
|
var cmdsubst []rune
|
|
var buf bytes.Buffer
|
|
Loop:
|
|
for _, c := range s {
|
|
if skip != 0 {
|
|
if skip != c {
|
|
continue Loop
|
|
}
|
|
if len(cmdsubst) > 0 && cmdsubst[len(cmdsubst)-1] == skip {
|
|
cmdsubst = cmdsubst[:len(cmdsubst)-1]
|
|
}
|
|
skip = 0
|
|
}
|
|
if quote != 0 {
|
|
if quote == c && (quote == '\'' || !escape) {
|
|
quote = 0
|
|
}
|
|
} else if !escape {
|
|
if c == '#' && isWhitespace(lastch) {
|
|
if len(cmdsubst) == 0 {
|
|
// strip comment until the end of line.
|
|
skip = '\n'
|
|
continue Loop
|
|
}
|
|
// strip comment until the end of command subst.
|
|
skip = cmdsubst[len(cmdsubst)-1]
|
|
continue Loop
|
|
} else if c == '\'' || c == '"' {
|
|
quote = c
|
|
} else if lastch == '$' && c == '(' {
|
|
cmdsubst = append(cmdsubst, ')')
|
|
} else if c == '`' {
|
|
cmdsubst = append(cmdsubst, '`')
|
|
}
|
|
}
|
|
if escape {
|
|
escape = false
|
|
} else if c == '\\' {
|
|
escape = true
|
|
} else {
|
|
escape = false
|
|
}
|
|
lastch = c
|
|
buf.WriteRune(c)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
var ccRE = regexp.MustCompile(`^prebuilts/(gcc|clang)/.*(gcc|g\+\+|clang|clang\+\+) .* ?-c `)
|
|
|
|
func gomaCmdForAndroidCompileCmd(cmd string) (string, bool) {
|
|
i := strings.Index(cmd, " ")
|
|
if i < 0 {
|
|
return cmd, false
|
|
}
|
|
driver := cmd[:i]
|
|
if strings.HasSuffix(driver, "ccache") {
|
|
return gomaCmdForAndroidCompileCmd(cmd[i+1:])
|
|
}
|
|
return cmd, ccRE.MatchString(cmd)
|
|
}
|
|
|
|
func descriptionFromCmd(cmd string) (string, bool) {
|
|
if !strings.HasPrefix(cmd, "echo") || !isWhitespace(rune(cmd[4])) {
|
|
return "", false
|
|
}
|
|
echoarg := cmd[5:]
|
|
|
|
// strip outer quotes, and fail if it is not a single echo command.
|
|
var buf bytes.Buffer
|
|
var escape bool
|
|
var quote rune
|
|
for _, c := range echoarg {
|
|
if escape {
|
|
escape = false
|
|
buf.WriteRune(c)
|
|
continue
|
|
}
|
|
if c == '\\' {
|
|
escape = true
|
|
buf.WriteRune(c)
|
|
continue
|
|
}
|
|
if quote != 0 {
|
|
if c == quote {
|
|
quote = 0
|
|
continue
|
|
}
|
|
buf.WriteRune(c)
|
|
continue
|
|
}
|
|
switch c {
|
|
case '\'', '"', '`':
|
|
quote = c
|
|
case '<', '>', '&', '|', ';':
|
|
return "", false
|
|
default:
|
|
buf.WriteRune(c)
|
|
}
|
|
}
|
|
return buf.String(), true
|
|
}
|
|
|
|
func (n *NinjaGenerator) genShellScript(runners []runner) (cmd string, desc string, useLocalPool bool) {
|
|
const defaultDesc = "build $out"
|
|
var useGomacc bool
|
|
var buf bytes.Buffer
|
|
for i, r := range runners {
|
|
if i > 0 {
|
|
if runners[i-1].ignoreError {
|
|
buf.WriteString(" ; ")
|
|
} else {
|
|
buf.WriteString(" && ")
|
|
}
|
|
}
|
|
cmd := trimTailingSlash(r.cmd)
|
|
cmd = stripShellComment(cmd)
|
|
cmd = trimLeftSpace(cmd)
|
|
cmd = strings.Replace(cmd, "\\\n\t", "", -1)
|
|
cmd = strings.Replace(cmd, "\\\n", "", -1)
|
|
cmd = strings.TrimRight(cmd, " \t\n;")
|
|
cmd = escapeNinja(cmd)
|
|
if cmd == "" {
|
|
cmd = "true"
|
|
}
|
|
glog.V(2).Infof("cmd %q=>%q", r.cmd, cmd)
|
|
if n.GomaDir != "" {
|
|
rcmd, ok := gomaCmdForAndroidCompileCmd(cmd)
|
|
if ok {
|
|
cmd = fmt.Sprintf("%s/gomacc %s", n.GomaDir, rcmd)
|
|
useGomacc = true
|
|
}
|
|
}
|
|
if n.DetectAndroidEcho && desc == "" {
|
|
d, ok := descriptionFromCmd(cmd)
|
|
if ok {
|
|
desc = d
|
|
cmd = "true"
|
|
}
|
|
}
|
|
needsSubShell := i > 0 || len(runners) > 1
|
|
if cmd[0] == '(' {
|
|
needsSubShell = false
|
|
}
|
|
|
|
if needsSubShell {
|
|
buf.WriteByte('(')
|
|
}
|
|
buf.WriteString(cmd)
|
|
if i == len(runners)-1 && r.ignoreError {
|
|
buf.WriteString(" ; true")
|
|
}
|
|
if needsSubShell {
|
|
buf.WriteByte(')')
|
|
}
|
|
}
|
|
if desc == "" {
|
|
desc = defaultDesc
|
|
}
|
|
return buf.String(), desc, n.GomaDir != "" && !useGomacc
|
|
}
|
|
|
|
func (n *NinjaGenerator) genRuleName() string {
|
|
ruleName := fmt.Sprintf("rule%d", n.ruleID)
|
|
n.ruleID++
|
|
return ruleName
|
|
}
|
|
|
|
func (n *NinjaGenerator) emitBuild(output, rule, inputs, orderOnlys string) {
|
|
fmt.Fprintf(n.f, "build %s: %s", escapeBuildTarget(output), rule)
|
|
if inputs != "" {
|
|
fmt.Fprintf(n.f, " %s", inputs)
|
|
}
|
|
if orderOnlys != "" {
|
|
fmt.Fprintf(n.f, " || %s", orderOnlys)
|
|
}
|
|
}
|
|
|
|
func escapeBuildTarget(s string) string {
|
|
i := strings.IndexAny(s, "$: \\")
|
|
if i < 0 {
|
|
return s
|
|
}
|
|
// unescapeInput only "\ ", "\=" unescape as " ", "=".
|
|
// TODO(ukai): which char should unescape, which should not here?
|
|
var esc rune
|
|
var buf bytes.Buffer
|
|
for _, c := range s {
|
|
switch c {
|
|
case '\\':
|
|
esc = c
|
|
continue
|
|
case '$', ':', ' ':
|
|
esc = 0
|
|
buf.WriteByte('$')
|
|
}
|
|
if esc != 0 {
|
|
buf.WriteRune(esc)
|
|
esc = 0
|
|
}
|
|
buf.WriteRune(c)
|
|
}
|
|
if esc != 0 {
|
|
buf.WriteRune(esc)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func (n *NinjaGenerator) dependency(node *DepNode) (string, string) {
|
|
var deps []string
|
|
seen := make(map[string]bool)
|
|
for _, d := range node.Deps {
|
|
t := escapeBuildTarget(d.Output)
|
|
if seen[t] {
|
|
continue
|
|
}
|
|
deps = append(deps, t)
|
|
seen[t] = true
|
|
}
|
|
var orderOnlys []string
|
|
for _, d := range node.OrderOnlys {
|
|
t := escapeBuildTarget(d.Output)
|
|
if seen[t] {
|
|
continue
|
|
}
|
|
orderOnlys = append(orderOnlys, t)
|
|
seen[t] = true
|
|
}
|
|
return strings.Join(deps, " "), strings.Join(orderOnlys, " ")
|
|
}
|
|
|
|
func escapeNinja(s string) string {
|
|
return strings.Replace(s, "$", "$$", -1)
|
|
}
|
|
|
|
func escapeShell(s string) string {
|
|
i := strings.IndexAny(s, "$`!\\\"")
|
|
if i < 0 {
|
|
return s
|
|
}
|
|
var buf bytes.Buffer
|
|
var lastDollar bool
|
|
for _, c := range s {
|
|
switch c {
|
|
case '$':
|
|
if lastDollar {
|
|
buf.WriteRune(c)
|
|
lastDollar = false
|
|
continue
|
|
}
|
|
buf.WriteString(`\$`)
|
|
lastDollar = true
|
|
continue
|
|
case '`', '"', '!', '\\':
|
|
buf.WriteByte('\\')
|
|
}
|
|
buf.WriteRune(c)
|
|
lastDollar = false
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func (n *NinjaGenerator) ninjaVars(s string, nv [][]string, esc func(string) string) string {
|
|
for _, v := range nv {
|
|
k, v := v[0], v[1]
|
|
if v == "" {
|
|
continue
|
|
}
|
|
if strings.Contains(v, "/./") || strings.Contains(v, "/../") || strings.Contains(v, "$") {
|
|
// ninja will normalize paths (/./, /../), so keep it as is
|
|
// ninja will emit quoted string for $
|
|
continue
|
|
}
|
|
if esc != nil {
|
|
v = esc(v)
|
|
}
|
|
s = strings.Replace(s, v, k, -1)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (n *NinjaGenerator) emitNode(node *DepNode) error {
|
|
output := node.Output
|
|
if _, found := n.done[output]; found {
|
|
return nil
|
|
}
|
|
n.done[output] = nodeVisit
|
|
|
|
if len(node.Cmds) == 0 && len(node.Deps) == 0 && len(node.OrderOnlys) == 0 && !node.IsPhony {
|
|
if _, ok := n.ctx.vpaths.exists(output); ok {
|
|
n.done[output] = nodeFile
|
|
return nil
|
|
}
|
|
o := filepath.Clean(output)
|
|
if o != output {
|
|
// if normalized target has been done, it marks as alias.
|
|
if s, found := n.done[o]; found {
|
|
glog.V(1).Infof("node %s=%s => %s=alias", o, s, node.Output)
|
|
n.done[output] = nodeAlias
|
|
return nil
|
|
}
|
|
}
|
|
if node.Filename == "" {
|
|
n.done[output] = nodeMissing
|
|
}
|
|
return nil
|
|
}
|
|
|
|
runners, _, err := createRunners(n.ctx, node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ruleName := "phony"
|
|
useLocalPool := false
|
|
inputs, orderOnlys := n.dependency(node)
|
|
if len(runners) > 0 {
|
|
ruleName = n.genRuleName()
|
|
fmt.Fprintf(n.f, "\n# rule for %q\n", node.Output)
|
|
fmt.Fprintf(n.f, "rule %s\n", ruleName)
|
|
|
|
ss, desc, ulp := n.genShellScript(runners)
|
|
if ulp {
|
|
useLocalPool = true
|
|
}
|
|
fmt.Fprintf(n.f, " description = %s\n", desc)
|
|
cmdline, depfile, err := getDepfile(ss)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if depfile != "" {
|
|
fmt.Fprintf(n.f, " depfile = %s\n", depfile)
|
|
fmt.Fprintf(n.f, " deps = gcc\n")
|
|
}
|
|
nv := [][]string{
|
|
[]string{"${in}", inputs},
|
|
[]string{"${out}", escapeNinja(output)},
|
|
}
|
|
// It seems Linux is OK with ~130kB.
|
|
// TODO: Find this number automatically.
|
|
ArgLenLimit := 100 * 1000
|
|
if len(cmdline) > ArgLenLimit {
|
|
fmt.Fprintf(n.f, " rspfile = $out.rsp\n")
|
|
cmdline = n.ninjaVars(cmdline, nv, nil)
|
|
fmt.Fprintf(n.f, " rspfile_content = %s\n", cmdline)
|
|
fmt.Fprintf(n.f, " command = %s $out.rsp\n", n.ctx.shell)
|
|
} else {
|
|
cmdline = escapeShell(cmdline)
|
|
cmdline = n.ninjaVars(cmdline, nv, escapeShell)
|
|
fmt.Fprintf(n.f, " command = %s -c \"%s\"\n", n.ctx.shell, cmdline)
|
|
}
|
|
}
|
|
n.emitBuild(output, ruleName, inputs, orderOnlys)
|
|
if useLocalPool {
|
|
fmt.Fprintf(n.f, " pool = local_pool\n")
|
|
}
|
|
fmt.Fprintf(n.f, "\n")
|
|
n.done[output] = nodeBuild
|
|
|
|
for _, d := range node.Deps {
|
|
err := n.emitNode(d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
glog.V(1).Infof("node %s dep node %q %s", node.Output, d.Output, n.done[d.Output])
|
|
}
|
|
for _, d := range node.OrderOnlys {
|
|
err := n.emitNode(d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
glog.V(1).Infof("node %s order node %q %s", node.Output, d.Output, n.done[d.Output])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (n *NinjaGenerator) emitRegenRules() error {
|
|
if len(n.Args) == 0 {
|
|
return nil
|
|
}
|
|
mkfiles, err := n.ctx.ev.EvaluateVar("MAKEFILE_LIST")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(n.f, `
|
|
rule regen_ninja
|
|
description = Regenerate ninja files due to dependency
|
|
generator=1
|
|
command=%s
|
|
`, strings.Join(n.Args, " "))
|
|
fmt.Fprintf(n.f, "build %s: regen_ninja %s", n.ninjaName(), mkfiles)
|
|
// TODO: Add dependencies to directories read by $(wildcard) or
|
|
// $(shell find).
|
|
if len(usedEnvs) > 0 {
|
|
fmt.Fprintf(n.f, " %s", n.envlistName())
|
|
}
|
|
fmt.Fprintf(n.f, "\n\n")
|
|
return nil
|
|
}
|
|
|
|
func (n *NinjaGenerator) shName() string {
|
|
return fmt.Sprintf("ninja%s.sh", n.Suffix)
|
|
}
|
|
|
|
func (n *NinjaGenerator) ninjaName() string {
|
|
return fmt.Sprintf("build%s.ninja", n.Suffix)
|
|
}
|
|
|
|
func (n *NinjaGenerator) envlistName() string {
|
|
return fmt.Sprintf(".kati_env%s", n.Suffix)
|
|
}
|
|
|
|
func (n *NinjaGenerator) generateEnvlist() (err error) {
|
|
f, err := os.Create(n.envlistName())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
cerr := f.Close()
|
|
if err == nil {
|
|
err = cerr
|
|
}
|
|
}()
|
|
for k := range usedEnvs {
|
|
v, err := n.ctx.ev.EvaluateVar(k)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(f, "%q=%q\n", k, v)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (n *NinjaGenerator) generateShell() (err error) {
|
|
f, err := os.Create(n.shName())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
cerr := f.Close()
|
|
if err == nil {
|
|
err = cerr
|
|
}
|
|
}()
|
|
|
|
fmt.Fprintf(f, "#!/bin/bash\n")
|
|
fmt.Fprintf(f, "# Generated by kati %s\n", gitVersion)
|
|
fmt.Fprintln(f)
|
|
fmt.Fprintln(f, `cd $(dirname "$0")`)
|
|
if n.Suffix != "" {
|
|
fmt.Fprintf(f, "if [ -f %s ]; then\n export $(cat %s)\nfi\n", n.envlistName(), n.envlistName())
|
|
}
|
|
for name, export := range n.exports {
|
|
// export "a b"=c will error on bash
|
|
// bash: export `a b=c': not a valid identifier
|
|
if strings.ContainsAny(name, " \t\n\r") {
|
|
glog.V(1).Infof("ignore export %q (export:%t)", name, export)
|
|
continue
|
|
}
|
|
if export {
|
|
v, err := n.ctx.ev.EvaluateVar(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(f, "export %q=%q\n", name, v)
|
|
} else {
|
|
fmt.Fprintf(f, "unset %q\n", name)
|
|
}
|
|
}
|
|
if n.GomaDir == "" {
|
|
fmt.Fprintf(f, `exec ninja -f %s "$@"`+"\n", n.ninjaName())
|
|
} else {
|
|
fmt.Fprintf(f, `exec ninja -f %s -j500 "$@"`+"\n", n.ninjaName())
|
|
}
|
|
|
|
return f.Chmod(0755)
|
|
}
|
|
|
|
func (n *NinjaGenerator) generateNinja(defaultTarget string) (err error) {
|
|
f, err := os.Create(n.ninjaName())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
cerr := f.Close()
|
|
if err == nil {
|
|
err = cerr
|
|
}
|
|
}()
|
|
|
|
n.f = f
|
|
fmt.Fprintf(n.f, "# Generated by kati %s\n", gitVersion)
|
|
fmt.Fprintf(n.f, "\n")
|
|
|
|
if len(usedEnvs) > 0 {
|
|
fmt.Fprintln(n.f, "# Environment variables used:")
|
|
var names []string
|
|
for name := range usedEnvs {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
v, err := n.ctx.ev.EvaluateVar(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(n.f, "# %q=%q\n", name, v)
|
|
}
|
|
fmt.Fprintf(n.f, "\n")
|
|
}
|
|
|
|
if n.GomaDir != "" {
|
|
fmt.Fprintf(n.f, "pool local_pool\n")
|
|
fmt.Fprintf(n.f, " depth = %d\n\n", runtime.NumCPU())
|
|
}
|
|
|
|
err = n.emitRegenRules()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// defining $out for $@ and $in for $^ here doesn't work well,
|
|
// because these texts will be processed in escapeShell...
|
|
for _, node := range n.nodes {
|
|
err := n.emitNode(node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
glog.V(1).Infof("node %q %s", node.Output, n.done[node.Output])
|
|
}
|
|
|
|
// emit phony targets for visited nodes that are
|
|
// - not existing file
|
|
// - not alias for other targets.
|
|
var nodes []string
|
|
for node, state := range n.done {
|
|
if state != nodeVisit {
|
|
continue
|
|
}
|
|
nodes = append(nodes, node)
|
|
}
|
|
if len(nodes) > 0 {
|
|
fmt.Fprintln(n.f)
|
|
sort.Strings(nodes)
|
|
for _, node := range nodes {
|
|
n.emitBuild(node, "phony", "", "")
|
|
fmt.Fprintln(n.f)
|
|
n.done[node] = nodeBuild
|
|
}
|
|
}
|
|
|
|
// emit default if the target was emitted.
|
|
if defaultTarget != "" && n.done[defaultTarget] == nodeBuild {
|
|
fmt.Fprintf(n.f, "\ndefault %s\n", escapeNinja(defaultTarget))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Save generates build.ninja from DepGraph.
|
|
func (n *NinjaGenerator) Save(g *DepGraph, name string, targets []string) error {
|
|
startTime := time.Now()
|
|
n.init(g)
|
|
err := n.generateEnvlist()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = n.generateShell()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var defaultTarget string
|
|
if len(targets) == 0 && len(g.nodes) > 0 {
|
|
defaultTarget = g.nodes[0].Output
|
|
}
|
|
err = n.generateNinja(defaultTarget)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logStats("generate ninja time: %q", time.Since(startTime))
|
|
return nil
|
|
}
|