// 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
}