summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNico Weber <nicolasweber@gmx.de>2014-06-27 20:19:46 (GMT)
committerNico Weber <nicolasweber@gmx.de>2014-06-27 20:19:46 (GMT)
commit69bfacebae8315de585837f625739e7621fa38d4 (patch)
tree8a61e69a62ebc56f7b96463c910975d58ba09e7c
parent63d5b1013cafb2db95687cf446eb5bb68cf6a27a (diff)
parent7c231a3d0d800bfc2602da097b34d1edca2f600f (diff)
downloadNinja-69bfacebae8315de585837f625739e7621fa38d4.zip
Ninja-69bfacebae8315de585837f625739e7621fa38d4.tar.gz
Ninja-69bfacebae8315de585837f625739e7621fa38d4.tar.bz2
v1.5.0v1.5.0
-rw-r--r--.clang-format25
-rw-r--r--.gitignore3
-rw-r--r--RELEASING17
-rwxr-xr-xbootstrap.py13
-rwxr-xr-xconfigure.py30
-rw-r--r--doc/manual.asciidoc37
-rw-r--r--misc/bash-completion54
-rw-r--r--misc/ninja-mode.el18
-rw-r--r--misc/ninja.vim8
-rw-r--r--misc/ninja_syntax.py44
-rw-r--r--misc/write_fake_manifests.py219
-rw-r--r--misc/zsh-completion47
-rw-r--r--platform_helper.py16
-rw-r--r--src/build.cc31
-rw-r--r--src/build.h5
-rw-r--r--src/build_log.cc17
-rw-r--r--src/build_log.h11
-rw-r--r--src/build_log_perftest.cc7
-rw-r--r--src/build_log_test.cc53
-rw-r--r--src/build_test.cc113
-rw-r--r--src/clean.cc4
-rw-r--r--src/clean_test.cc36
-rw-r--r--src/debug_flags.cc2
-rw-r--r--src/debug_flags.h2
-rw-r--r--src/depfile_parser.cc87
-rw-r--r--src/depfile_parser.in.cc6
-rw-r--r--src/depfile_parser_perftest.cc (renamed from src/parser_perftest.cc)0
-rw-r--r--src/depfile_parser_test.cc19
-rw-r--r--src/deps_log.cc13
-rw-r--r--src/deps_log.h8
-rw-r--r--src/deps_log_test.cc57
-rw-r--r--src/disk_interface.cc135
-rw-r--r--src/disk_interface.h26
-rw-r--r--src/disk_interface_test.cc48
-rw-r--r--src/edit_distance.cc1
-rw-r--r--src/graph.cc34
-rw-r--r--src/graph.h7
-rw-r--r--src/graph_test.cc11
-rw-r--r--src/hash_map.h1
-rw-r--r--src/includes_normalize_test.cc2
-rw-r--r--src/line_printer.cc51
-rw-r--r--src/line_printer.h20
-rw-r--r--src/manifest_parser.cc9
-rw-r--r--src/manifest_parser_perftest.cc118
-rw-r--r--src/manifest_parser_test.cc19
-rw-r--r--src/metrics.cc2
-rw-r--r--src/msvc_helper-win32.cc19
-rw-r--r--src/msvc_helper.h5
-rw-r--r--src/msvc_helper_main-win32.cc9
-rw-r--r--src/msvc_helper_test.cc36
-rw-r--r--src/ninja.cc40
-rw-r--r--src/state.cc6
-rw-r--r--src/state.h3
-rw-r--r--src/subprocess-posix.cc53
-rw-r--r--src/subprocess-win32.cc30
-rw-r--r--src/subprocess.h5
-rw-r--r--src/subprocess_test.cc15
-rw-r--r--src/test.cc4
-rw-r--r--src/test.h2
-rw-r--r--src/util.cc104
-rw-r--r--src/util.h7
-rw-r--r--src/util_test.cc31
-rw-r--r--src/version.cc2
63 files changed, 1547 insertions, 310 deletions
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..1841c03
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,25 @@
+# Copyright 2014 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This isn't meant to be authoritative, but it's good enough to be useful.
+# Still use your best judgement for formatting decisions: clang-format
+# sometimes makes strange choices.
+
+BasedOnStyle: Google
+AllowShortFunctionsOnASingleLine: Inline
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+Cpp11BracedListStyle: false
+IndentCaseLabels: false
diff --git a/.gitignore b/.gitignore
index 501a02d..40a610d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,9 +8,10 @@ TAGS
/ninja
/build_log_perftest
/canon_perftest
+/depfile_parser_perftest
/hash_collision_bench
/ninja_test
-/parser_perftest
+/manifest_parser_perftest
/graph.png
/doc/manual.html
/doc/doxygen
diff --git a/RELEASING b/RELEASING
index 25926db..c4973b2 100644
--- a/RELEASING
+++ b/RELEASING
@@ -1,17 +1,18 @@
Notes to myself on all the steps to make for a Ninja release.
Push new release branch:
-1. update src/version.cc with new version (with ".git")
-2. git checkout release; git merge master
-3. fix version number in src/version.cc (it will likely conflict in the above)
-4. fix version in doc/manual.asciidoc
-5. commit, tag, push (don't forget to push --tags)
-6. construct release notes from prior notes
+1. Consider sending a heads-up to the ninja-build mailing list first
+2. update src/version.cc with new version (with ".git"), commit to master
+3. git checkout release; git merge master
+4. fix version number in src/version.cc (it will likely conflict in the above)
+5. fix version in doc/manual.asciidoc
+6. commit, tag, push (don't forget to push --tags)
+7. construct release notes from prior notes
credits: git shortlog -s --no-merges REV..
Release on github:
-1. (haven't tried this yet)
- https://github.com/blog/1547-release-your-software
+1. https://github.com/blog/1547-release-your-software
+ Add binaries to https://github.com/martine/ninja/releases
Make announcement on mailing list:
1. copy old mail
diff --git a/bootstrap.py b/bootstrap.py
index 66ec85b..026396b 100755
--- a/bootstrap.py
+++ b/bootstrap.py
@@ -34,10 +34,12 @@ parser.add_option('--verbose', action='store_true',
parser.add_option('--x64', action='store_true',
help='force 64-bit build (Windows)',)
parser.add_option('--platform',
- help='target platform (' + '/'.join(platform_helper.platforms()) + ')',
+ help='target platform (' +
+ '/'.join(platform_helper.platforms()) + ')',
choices=platform_helper.platforms())
parser.add_option('--force-pselect', action='store_true',
- help="ppoll() is used by default on Linux, OpenBSD and Bitrig, but older versions might need to use pselect instead",)
+ help='ppoll() is used by default where available, '
+ 'but some platforms might need to use pselect instead',)
(options, conf_args) = parser.parse_args()
@@ -109,7 +111,8 @@ else:
cflags.append('-D_WIN32_WINNT=0x0501')
if options.x64:
cflags.append('-m64')
-if (platform.is_linux() or platform.is_openbsd() or platform.is_bitrig()) and not options.force_pselect:
+if (platform.is_linux() or platform.is_openbsd() or platform.is_bitrig()) and \
+ not options.force_pselect:
cflags.append('-DUSE_PPOLL')
if options.force_pselect:
conf_args.append("--force-pselect")
@@ -153,8 +156,8 @@ if platform.is_windows():
Done!
Note: to work around Windows file locking, where you can't rebuild an
-in-use binary, to run ninja after making any changes to build ninja itself
-you should run ninja.bootstrap instead.""")
+in-use binary, to run ninja after making any changes to build ninja
+itself you should run ninja.bootstrap instead.""")
else:
print('Building ninja using itself...')
run([sys.executable, 'configure.py'] + conf_args)
diff --git a/configure.py b/configure.py
index 9fe3be8..64123a0 100755
--- a/configure.py
+++ b/configure.py
@@ -32,10 +32,12 @@ import ninja_syntax
parser = OptionParser()
profilers = ['gmon', 'pprof']
parser.add_option('--platform',
- help='target platform (' + '/'.join(platform_helper.platforms()) + ')',
+ help='target platform (' +
+ '/'.join(platform_helper.platforms()) + ')',
choices=platform_helper.platforms())
parser.add_option('--host',
- help='host platform (' + '/'.join(platform_helper.platforms()) + ')',
+ help='host platform (' +
+ '/'.join(platform_helper.platforms()) + ')',
choices=platform_helper.platforms())
parser.add_option('--debug', action='store_true',
help='enable debugging extras',)
@@ -48,7 +50,8 @@ parser.add_option('--with-python', metavar='EXE',
help='use EXE as the Python interpreter',
default=os.path.basename(sys.executable))
parser.add_option('--force-pselect', action='store_true',
- help="ppoll() is used by default where available, but some platforms may need to use pselect instead",)
+ help='ppoll() is used by default where available, '
+ 'but some platforms may need to use pselect instead',)
(options, args) = parser.parse_args()
if args:
print('ERROR: extra unparsed command-line arguments:', args)
@@ -125,6 +128,8 @@ if platform.is_msvc():
'/DNOMINMAX', '/D_CRT_SECURE_NO_WARNINGS',
'/D_VARIADIC_MAX=10',
'/DNINJA_PYTHON="%s"' % options.with_python]
+ if platform.msvc_needs_fs():
+ cflags.append('/FS')
ldflags = ['/DEBUG', '/libpath:$builddir']
if not options.debug:
cflags += ['/Ox', '/DNDEBUG', '/GL']
@@ -165,7 +170,8 @@ else:
cflags.append('-fno-omit-frame-pointer')
libs.extend(['-Wl,--no-as-needed', '-lprofiler'])
-if (platform.is_linux() or platform.is_openbsd() or platform.is_bitrig()) and not options.force_pselect:
+if (platform.is_linux() or platform.is_openbsd() or platform.is_bitrig()) and \
+ not options.force_pselect:
cflags.append('-DUSE_PPOLL')
def shell_escape(str):
@@ -322,7 +328,10 @@ if options.with_gtest:
gtest_all_incs = '-I%s -I%s' % (path, os.path.join(path, 'include'))
if platform.is_msvc():
- gtest_cflags = '/nologo /EHsc /Zi /D_VARIADIC_MAX=10 ' + gtest_all_incs
+ gtest_cflags = '/nologo /EHsc /Zi /D_VARIADIC_MAX=10 '
+ if platform.msvc_needs_fs():
+ gtest_cflags += '/FS '
+ gtest_cflags += gtest_all_incs
else:
gtest_cflags = '-fvisibility=hidden ' + gtest_all_incs
objs += n.build(built('gtest-all' + objext), 'cxx',
@@ -356,7 +365,7 @@ for name in ['build_log_test',
objs += cxx(name, variables=[('cflags', '$test_cflags')])
if platform.is_windows():
for name in ['includes_normalize_test', 'msvc_helper_test']:
- objs += cxx(name, variables=[('cflags', test_cflags)])
+ objs += cxx(name, variables=[('cflags', '$test_cflags')])
if not platform.is_windows():
test_libs.append('-lpthread')
@@ -368,18 +377,21 @@ all_targets += ninja_test
n.comment('Ancillary executables.')
-objs = cxx('parser_perftest')
-all_targets += n.build(binary('parser_perftest'), 'link', objs,
- implicit=ninja_lib, variables=[('libs', libs)])
objs = cxx('build_log_perftest')
all_targets += n.build(binary('build_log_perftest'), 'link', objs,
implicit=ninja_lib, variables=[('libs', libs)])
objs = cxx('canon_perftest')
all_targets += n.build(binary('canon_perftest'), 'link', objs,
implicit=ninja_lib, variables=[('libs', libs)])
+objs = cxx('depfile_parser_perftest')
+all_targets += n.build(binary('depfile_parser_perftest'), 'link', objs,
+ implicit=ninja_lib, variables=[('libs', libs)])
objs = cxx('hash_collision_bench')
all_targets += n.build(binary('hash_collision_bench'), 'link', objs,
implicit=ninja_lib, variables=[('libs', libs)])
+objs = cxx('manifest_parser_perftest')
+all_targets += n.build(binary('manifest_parser_perftest'), 'link', objs,
+ implicit=ninja_lib, variables=[('libs', libs)])
n.newline()
n.comment('Generate a graph using the "graph" tool.')
diff --git a/doc/manual.asciidoc b/doc/manual.asciidoc
index 6b2296f..bacb5f6 100644
--- a/doc/manual.asciidoc
+++ b/doc/manual.asciidoc
@@ -1,7 +1,7 @@
Ninja
=====
Evan Martin <martine@danga.com>
-v1.4.0, September 2013
+v1.5.0, June 2014
Introduction
@@ -581,9 +581,13 @@ Ninja supports this processing in two forms.
http://msdn.microsoft.com/en-us/library/hdkef6tk(v=vs.90).aspx[`/showIncludes`
flag]. Briefly, this means the tool outputs specially-formatted lines
to its stdout. Ninja then filters these lines from the displayed
- output. No `depfile` attribute is necessary.
+ output. No `depfile` attribute is necessary, but the localized string
+ in front of the the header file path. For instance
+ `msvc_deps_prefix = Note: including file: `
+ for a English Visual Studio (the default). Should be globally defined.
+
----
+msvc_deps_prefix = Note: including file:
rule cc
deps = msvc
command = cl /showIncludes -c $in /Fo$out
@@ -645,6 +649,21 @@ build heavy_object2.obj: cc heavy_obj2.cc
----------------
+The `console` pool
+^^^^^^^^^^^^^^^^^^
+
+_Available since Ninja 1.5._
+
+There exists a pre-defined pool named `console` with a depth of 1. It has
+the special property that any task in the pool has direct access to the
+standard input, output and error streams provided to Ninja, which are
+normally connected to the user's console (hence the name) but could be
+redirected. This can be useful for interactive tasks or long-running tasks
+which produce status updates on the console (such as test suites).
+
+While a task in the `console` pool is running, Ninja's regular output (such
+as progress status and output from concurrent tasks) is buffered until
+it completes.
Ninja file reference
--------------------
@@ -773,6 +792,10 @@ keys.
stored as `.ninja_deps` in the `builddir`, see <<ref_toplevel,the
discussion of `builddir`>>.
+`msvc_deps_prefix`:: _(Available since Ninja 1.5.)_ defines the string
+ which should be stripped from msvc's /showIncludes output. Only
+ needed when `deps = msvc` and no English Visual Studio version is used.
+
`description`:: a short description of the command, used to pretty-print
the command as it's running. The `-v` flag controls whether to print
the full command or its description; if a command fails, the full command
@@ -784,9 +807,9 @@ keys.
rebuilt if the command line changes; and secondly, they are not
cleaned by default.
-`in`:: the shell-quoted space-separated list of files provided as
- inputs to the build line referencing this `rule`. (`$in` is provided
- solely for convenience; if you need some subset or variant of this
+`in`:: the space-separated list of files provided as inputs to the build line
+ referencing this `rule`, shell-quoted if it appears in commands. (`$in` is
+ provided solely for convenience; if you need some subset or variant of this
list of files, just construct a new variable with that list and use
that instead.)
@@ -795,8 +818,8 @@ keys.
`$rspfile_content`; this works around a bug in the MSVC linker where
it uses a fixed-size buffer for processing input.)
-`out`:: the shell-quoted space-separated list of files provided as
- outputs to the build line referencing this `rule`.
+`out`:: the space-separated list of files provided as outputs to the build line
+ referencing this `rule`, shell-quoted if it appears in commands.
`restat`:: if present, causes Ninja to re-stat the command's outputs
after execution of the command. Each output whose modification time
diff --git a/misc/bash-completion b/misc/bash-completion
index 2d6975b..6edf4df 100644
--- a/misc/bash-completion
+++ b/misc/bash-completion
@@ -16,25 +16,43 @@
# . path/to/ninja/misc/bash-completion
_ninja_target() {
- local cur targets dir line targets_command OPTIND
- cur="${COMP_WORDS[COMP_CWORD]}"
+ local cur prev targets dir line targets_command OPTIND
- if [[ "$cur" == "--"* ]]; then
- # there is currently only one argument that takes --
- COMPREPLY=($(compgen -P '--' -W 'version' -- "${cur:2}"))
- else
- dir="."
- line=$(echo ${COMP_LINE} | cut -d" " -f 2-)
- # filter out all non relevant arguments but keep C for dirs
- while getopts C:f:j:l:k:nvd:t: opt "${line[@]}"; do
- case $opt in
- C) dir="$OPTARG" ;;
- esac
- done;
- targets_command="ninja -C ${dir} -t targets all"
- targets=$((${targets_command} 2>/dev/null) | awk -F: '{print $1}')
- COMPREPLY=($(compgen -W "$targets" -- "$cur"))
- fi
+ # When available, use bash_completion to:
+ # 1) Complete words when the cursor is in the middle of the word
+ # 2) Complete paths with files or directories, as appropriate
+ if _get_comp_words_by_ref cur prev &>/dev/null ; then
+ case $prev in
+ -f)
+ _filedir
+ return 0
+ ;;
+ -C)
+ _filedir -d
+ return 0
+ ;;
+ esac
+ else
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ fi
+
+ if [[ "$cur" == "--"* ]]; then
+ # there is currently only one argument that takes --
+ COMPREPLY=($(compgen -P '--' -W 'version' -- "${cur:2}"))
+ else
+ dir="."
+ line=$(echo ${COMP_LINE} | cut -d" " -f 2-)
+ # filter out all non relevant arguments but keep C for dirs
+ while getopts :C:f:j:l:k:nvd:t: opt $line; do
+ case $opt in
+ # eval for tilde expansion
+ C) eval dir="$OPTARG" ;;
+ esac
+ done;
+ targets_command="eval ninja -C \"${dir}\" -t targets all"
+ targets=$((${targets_command} 2>/dev/null) | awk -F: '{print $1}')
+ COMPREPLY=($(compgen -W "$targets" -- "$cur"))
+ fi
return
}
complete -F _ninja_target ninja
diff --git a/misc/ninja-mode.el b/misc/ninja-mode.el
index d939206..36ada6f 100644
--- a/misc/ninja-mode.el
+++ b/misc/ninja-mode.el
@@ -1,3 +1,5 @@
+;;; ninja-mode.el --- Major mode for editing .ninja files
+
;; Copyright 2011 Google Inc. All Rights Reserved.
;;
;; Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,10 +14,14 @@
;; See the License for the specific language governing permissions and
;; limitations under the License.
+;;; Commentary:
+
;; Simple emacs mode for editing .ninja files.
;; Just some syntax highlighting for now.
-(setq ninja-keywords
+;;; Code:
+
+(defvar ninja-keywords
(list
'("^#.*" . font-lock-comment-face)
(cons (concat "^" (regexp-opt '("rule" "build" "subninja" "include"
@@ -26,8 +32,10 @@
;; Variable expansion.
'("\\($[[:alnum:]_]+\\)" . (1 font-lock-variable-name-face))
;; Rule names
- '("rule \\([[:alnum:]_]+\\)" . (1 font-lock-function-name-face))
+ '("rule \\([[:alnum:]_-]+\\)" . (1 font-lock-function-name-face))
))
+
+;;;###autoload
(define-derived-mode ninja-mode fundamental-mode "ninja"
(setq comment-start "#")
; Pass extra "t" to turn off syntax-based fontification -- we don't want
@@ -35,8 +43,10 @@
(setq font-lock-defaults '(ninja-keywords t))
)
-(provide 'ninja-mode)
-
;; Run ninja-mode for files ending in .ninja.
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.ninja$" . ninja-mode))
+
+(provide 'ninja-mode)
+
+;;; ninja-mode.el ends here
diff --git a/misc/ninja.vim b/misc/ninja.vim
index d813267..f34588f 100644
--- a/misc/ninja.vim
+++ b/misc/ninja.vim
@@ -1,10 +1,10 @@
" ninja build file syntax.
" Language: ninja build file as described at
" http://martine.github.com/ninja/manual.html
-" Version: 1.3
-" Last Change: 2013/04/16
+" Version: 1.4
+" Last Change: 2014/05/13
" Maintainer: Nicolas Weber <nicolasweber@gmx.de>
-" Version 1.3 of this script is in the upstream vim repository and will be
+" Version 1.4 of this script is in the upstream vim repository and will be
" included in the next vim release. If you change this, please send your change
" upstream.
@@ -55,6 +55,7 @@ syn keyword ninjaPoolCommand contained depth
" $simple_varname -> variable
" ${varname} -> variable
+syn match ninjaDollar "\$\$"
syn match ninjaWrapLineOperator "\$$"
syn match ninjaSimpleVar "\$[a-zA-Z0-9_-]\+"
syn match ninjaVar "\${[a-zA-Z0-9_.-]\+}"
@@ -70,6 +71,7 @@ hi def link ninjaComment Comment
hi def link ninjaKeyword Keyword
hi def link ninjaRuleCommand Statement
hi def link ninjaPoolCommand Statement
+hi def link ninjaDollar ninjaOperator
hi def link ninjaWrapLineOperator ninjaOperator
hi def link ninjaOperator Operator
hi def link ninjaSimpleVar ninjaVar
diff --git a/misc/ninja_syntax.py b/misc/ninja_syntax.py
index d69e3e4..14b932f 100644
--- a/misc/ninja_syntax.py
+++ b/misc/ninja_syntax.py
@@ -8,10 +8,9 @@ use Python.
"""
import textwrap
-import re
def escape_path(word):
- return word.replace('$ ','$$ ').replace(' ','$ ').replace(':', '$:')
+ return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:')
class Writer(object):
def __init__(self, output, width=78):
@@ -61,21 +60,20 @@ class Writer(object):
def build(self, outputs, rule, inputs=None, implicit=None, order_only=None,
variables=None):
outputs = self._as_list(outputs)
- all_inputs = self._as_list(inputs)[:]
- out_outputs = list(map(escape_path, outputs))
- all_inputs = list(map(escape_path, all_inputs))
+ out_outputs = [escape_path(x) for x in outputs]
+ all_inputs = [escape_path(x) for x in self._as_list(inputs)]
if implicit:
- implicit = map(escape_path, self._as_list(implicit))
+ implicit = [escape_path(x) for x in self._as_list(implicit)]
all_inputs.append('|')
all_inputs.extend(implicit)
if order_only:
- order_only = map(escape_path, self._as_list(order_only))
+ order_only = [escape_path(x) for x in self._as_list(order_only)]
all_inputs.append('||')
all_inputs.extend(order_only)
self._line('build %s: %s' % (' '.join(out_outputs),
- ' '.join([rule] + all_inputs)))
+ ' '.join([rule] + all_inputs)))
if variables:
if isinstance(variables, dict):
@@ -98,13 +96,13 @@ class Writer(object):
self._line('default %s' % ' '.join(self._as_list(paths)))
def _count_dollars_before_index(self, s, i):
- """Returns the number of '$' characters right in front of s[i]."""
- dollar_count = 0
- dollar_index = i - 1
- while dollar_index > 0 and s[dollar_index] == '$':
- dollar_count += 1
- dollar_index -= 1
- return dollar_count
+ """Returns the number of '$' characters right in front of s[i]."""
+ dollar_count = 0
+ dollar_index = i - 1
+ while dollar_index > 0 and s[dollar_index] == '$':
+ dollar_count += 1
+ dollar_index -= 1
+ return dollar_count
def _line(self, text, indent=0):
"""Write 'text' word-wrapped at self.width characters."""
@@ -117,19 +115,19 @@ class Writer(object):
available_space = self.width - len(leading_space) - len(' $')
space = available_space
while True:
- space = text.rfind(' ', 0, space)
- if space < 0 or \
- self._count_dollars_before_index(text, space) % 2 == 0:
- break
+ space = text.rfind(' ', 0, space)
+ if (space < 0 or
+ self._count_dollars_before_index(text, space) % 2 == 0):
+ break
if space < 0:
# No such space; just use the first unescaped space we can find.
space = available_space - 1
while True:
- space = text.find(' ', space + 1)
- if space < 0 or \
- self._count_dollars_before_index(text, space) % 2 == 0:
- break
+ space = text.find(' ', space + 1)
+ if (space < 0 or
+ self._count_dollars_before_index(text, space) % 2 == 0):
+ break
if space < 0:
# Give up on breaking.
break
diff --git a/misc/write_fake_manifests.py b/misc/write_fake_manifests.py
new file mode 100644
index 0000000..837007e
--- /dev/null
+++ b/misc/write_fake_manifests.py
@@ -0,0 +1,219 @@
+#!/usr/bin/env python
+
+"""Writes large manifest files, for manifest parser performance testing.
+
+The generated manifest files are (eerily) similar in appearance and size to the
+ones used in the Chromium project.
+
+Usage:
+ python misc/write_fake_manifests.py outdir # Will run for about 5s.
+
+The program contains a hardcoded random seed, so it will generate the same
+output every time it runs. By changing the seed, it's easy to generate many
+different sets of manifest files.
+"""
+
+import argparse
+import contextlib
+import os
+import random
+import sys
+
+import ninja_syntax
+
+
+def paretoint(avg, alpha):
+ """Returns a random integer that's avg on average, following a power law.
+ alpha determines the shape of the power curve. alpha has to be larger
+ than 1. The closer alpha is to 1, the higher the variation of the returned
+ numbers."""
+ return int(random.paretovariate(alpha) * avg / (alpha / (alpha - 1)))
+
+
+# Based on http://neugierig.org/software/chromium/class-name-generator.html
+def moar(avg_options, p_suffix):
+ kStart = ['render', 'web', 'browser', 'tab', 'content', 'extension', 'url',
+ 'file', 'sync', 'content', 'http', 'profile']
+ kOption = ['view', 'host', 'holder', 'container', 'impl', 'ref',
+ 'delegate', 'widget', 'proxy', 'stub', 'context',
+ 'manager', 'master', 'watcher', 'service', 'file', 'data',
+ 'resource', 'device', 'info', 'provider', 'internals', 'tracker',
+ 'api', 'layer']
+ kOS = ['win', 'mac', 'aura', 'linux', 'android', 'unittest', 'browsertest']
+ num_options = min(paretoint(avg_options, alpha=4), 5)
+ # The original allows kOption to repeat as long as no consecutive options
+ # repeat. This version doesn't allow any option repetition.
+ name = [random.choice(kStart)] + random.sample(kOption, num_options)
+ if random.random() < p_suffix:
+ name.append(random.choice(kOS))
+ return '_'.join(name)
+
+
+class GenRandom(object):
+ def __init__(self):
+ self.seen_names = set([None])
+ self.seen_defines = set([None])
+
+ def _unique_string(self, seen, avg_options=1.3, p_suffix=0.1):
+ s = None
+ while s in seen:
+ s = moar(avg_options, p_suffix)
+ seen.add(s)
+ return s
+
+ def _n_unique_strings(self, n):
+ seen = set([None])
+ return [self._unique_string(seen, avg_options=3, p_suffix=0.4)
+ for _ in xrange(n)]
+
+ def target_name(self):
+ return self._unique_string(p_suffix=0, seen=self.seen_names)
+
+ def path(self):
+ return os.path.sep.join([
+ self._unique_string(self.seen_names, avg_options=1, p_suffix=0)
+ for _ in xrange(1 + paretoint(0.6, alpha=4))])
+
+ def src_obj_pairs(self, path, name):
+ num_sources = paretoint(55, alpha=2) + 1
+ return [(os.path.join('..', '..', path, s + '.cc'),
+ os.path.join('obj', path, '%s.%s.o' % (name, s)))
+ for s in self._n_unique_strings(num_sources)]
+
+ def defines(self):
+ return [
+ '-DENABLE_' + self._unique_string(self.seen_defines).upper()
+ for _ in xrange(paretoint(20, alpha=3))]
+
+
+LIB, EXE = 0, 1
+class Target(object):
+ def __init__(self, gen, kind):
+ self.name = gen.target_name()
+ self.dir_path = gen.path()
+ self.ninja_file_path = os.path.join(
+ 'obj', self.dir_path, self.name + '.ninja')
+ self.src_obj_pairs = gen.src_obj_pairs(self.dir_path, self.name)
+ if kind == LIB:
+ self.output = os.path.join('lib' + self.name + '.a')
+ elif kind == EXE:
+ self.output = os.path.join(self.name)
+ self.defines = gen.defines()
+ self.deps = []
+ self.kind = kind
+ self.has_compile_depends = random.random() < 0.4
+
+ @property
+ def includes(self):
+ return ['-I' + dep.dir_path for dep in self.deps]
+
+
+def write_target_ninja(ninja, target):
+ compile_depends = None
+ if target.has_compile_depends:
+ compile_depends = os.path.join(
+ 'obj', target.dir_path, target.name + '.stamp')
+ ninja.build(compile_depends, 'stamp', target.src_obj_pairs[0][0])
+ ninja.newline()
+
+ ninja.variable('defines', target.defines)
+ if target.deps:
+ ninja.variable('includes', target.includes)
+ ninja.variable('cflags', ['-Wall', '-fno-rtti', '-fno-exceptions'])
+ ninja.newline()
+
+ for src, obj in target.src_obj_pairs:
+ ninja.build(obj, 'cxx', src, implicit=compile_depends)
+ ninja.newline()
+
+ deps = [dep.output for dep in target.deps]
+ libs = [dep.output for dep in target.deps if dep.kind == LIB]
+ if target.kind == EXE:
+ ninja.variable('ldflags', '-Wl,pie')
+ ninja.variable('libs', libs)
+ link = { LIB: 'alink', EXE: 'link'}[target.kind]
+ ninja.build(target.output, link, [obj for _, obj in target.src_obj_pairs],
+ implicit=deps)
+
+
+def write_master_ninja(master_ninja, targets):
+ """Writes master build.ninja file, referencing all given subninjas."""
+ master_ninja.variable('cxx', 'c++')
+ master_ninja.variable('ld', '$cxx')
+ master_ninja.newline()
+
+ master_ninja.pool('link_pool', depth=4)
+ master_ninja.newline()
+
+ master_ninja.rule('cxx', description='CXX $out',
+ command='$cxx -MMD -MF $out.d $defines $includes $cflags -c $in -o $out',
+ depfile='$out.d', deps='gcc')
+ master_ninja.rule('alink', description='LIBTOOL-STATIC $out',
+ command='rm -f $out && libtool -static -o $out $in')
+ master_ninja.rule('link', description='LINK $out', pool='link_pool',
+ command='$ld $ldflags -o $out $in $libs')
+ master_ninja.rule('stamp', description='STAMP $out', command='touch $out')
+ master_ninja.newline()
+
+ for target in targets:
+ master_ninja.subninja(target.ninja_file_path)
+ master_ninja.newline()
+
+ master_ninja.comment('Short names for targets.')
+ for target in targets:
+ if target.name != target.output:
+ master_ninja.build(target.name, 'phony', target.output)
+ master_ninja.newline()
+
+ master_ninja.build('all', 'phony', [target.output for target in targets])
+ master_ninja.default('all')
+
+
+@contextlib.contextmanager
+def FileWriter(path):
+ """Context manager for a ninja_syntax object writing to a file."""
+ try:
+ os.makedirs(os.path.dirname(path))
+ except OSError:
+ pass
+ f = open(path, 'w')
+ yield ninja_syntax.Writer(f)
+ f.close()
+
+
+def random_targets():
+ num_targets = 800
+ gen = GenRandom()
+
+ # N-1 static libraries, and 1 executable depending on all of them.
+ targets = [Target(gen, LIB) for i in xrange(num_targets - 1)]
+ for i in range(len(targets)):
+ targets[i].deps = [t for t in targets[0:i] if random.random() < 0.05]
+
+ last_target = Target(gen, EXE)
+ last_target.deps = targets[:]
+ last_target.src_obj_pairs = last_target.src_obj_pairs[0:10] # Trim.
+ targets.append(last_target)
+ return targets
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('outdir', help='output directory')
+ args = parser.parse_args()
+ root_dir = args.outdir
+
+ random.seed(12345)
+
+ targets = random_targets()
+ for target in targets:
+ with FileWriter(os.path.join(root_dir, target.ninja_file_path)) as n:
+ write_target_ninja(n, target)
+
+ with FileWriter(os.path.join(root_dir, 'build.ninja')) as master_ninja:
+ master_ninja.width = 120
+ write_master_ninja(master_ninja, targets)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/misc/zsh-completion b/misc/zsh-completion
index cd0edfb..2fe16fb 100644
--- a/misc/zsh-completion
+++ b/misc/zsh-completion
@@ -1,3 +1,4 @@
+#compdef ninja
# Copyright 2011 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,7 +16,47 @@
# Add the following to your .zshrc to tab-complete ninja targets
# . path/to/ninja/misc/zsh-completion
-_ninja() {
- reply=(`(ninja -t targets all 2&>/dev/null) | awk -F: '{print $1}'`)
+__get_targets() {
+ ninja -t targets 2>/dev/null | while read -r a b; do echo $a | cut -d ':' -f1; done;
}
-compctl -K _ninja ninja
+
+__get_tools() {
+ ninja -t list 2>/dev/null | while read -r a b; do echo $a; done | tail -n +2
+}
+
+__get_modes() {
+ ninja -d list 2>/dev/null | while read -r a b; do echo $a; done | tail -n +2 | head -n -1
+}
+
+__modes() {
+ local -a modes
+ modes=(${(fo)"$(__get_modes)"})
+ _describe 'modes' modes
+}
+
+__tools() {
+ local -a tools
+ tools=(${(fo)"$(__get_tools)"})
+ _describe 'tools' tools
+}
+
+__targets() {
+ local -a targets
+ targets=(${(fo)"$(__get_targets)"})
+ _describe 'targets' targets
+}
+
+_arguments \
+ {-h,--help}'[Show help]' \
+ '--version[Print ninja version]' \
+ '-C+[Change to directory before doing anything else]:directories:_directories' \
+ '-f+[Specify input build file (default=build.ninja)]:files:_files' \
+ '-j+[Run N jobs in parallel (default=number of CPUs available)]:number of jobs' \
+ '-l+[Do not start new jobs if the load average is greater than N]:number of jobs' \
+ '-k+[Keep going until N jobs fail (default=1)]:number of jobs' \
+ '-n[Dry run (do not run commands but act like they succeeded)]' \
+ '-v[Show all command lines while building]' \
+ '-d+[Enable debugging (use -d list to list modes)]:modes:__modes' \
+ '-t+[Run a subtool (use -t list to list subtools)]:tools:__tools' \
+ '*::targets:__targets'
+
diff --git a/platform_helper.py b/platform_helper.py
index b7447a1..bc3a125 100644
--- a/platform_helper.py
+++ b/platform_helper.py
@@ -19,10 +19,10 @@ import sys
def platforms():
return ['linux', 'darwin', 'freebsd', 'openbsd', 'solaris', 'sunos5',
- 'mingw', 'msvc', 'gnukfreebsd8', 'bitrig']
+ 'mingw', 'msvc', 'gnukfreebsd', 'bitrig']
-class Platform( object ):
- def __init__( self, platform):
+class Platform(object):
+ def __init__(self, platform):
self._platform = platform
if not self._platform is None:
return
@@ -31,7 +31,7 @@ class Platform( object ):
self._platform = 'linux'
elif self._platform.startswith('freebsd'):
self._platform = 'freebsd'
- elif self._platform.startswith('gnukfreebsd8'):
+ elif self._platform.startswith('gnukfreebsd'):
self._platform = 'freebsd'
elif self._platform.startswith('openbsd'):
self._platform = 'openbsd'
@@ -56,6 +56,14 @@ class Platform( object ):
def is_msvc(self):
return self._platform == 'msvc'
+ def msvc_needs_fs(self):
+ import subprocess
+ popen = subprocess.Popen(['cl', '/nologo', '/?'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ out, err = popen.communicate()
+ return '/FS ' in str(out)
+
def is_windows(self):
return self.is_mingw() or self.is_msvc()
diff --git a/src/build.cc b/src/build.cc
index 9718f85..64bcea3 100644
--- a/src/build.cc
+++ b/src/build.cc
@@ -97,6 +97,9 @@ void BuildStatus::BuildEdgeStarted(Edge* edge) {
++started_edges_;
PrintStatus(edge);
+
+ if (edge->use_console())
+ printer_.SetConsoleLocked(true);
}
void BuildStatus::BuildEdgeFinished(Edge* edge,
@@ -112,10 +115,13 @@ void BuildStatus::BuildEdgeFinished(Edge* edge,
*end_time = (int)(now - start_time_millis_);
running_edges_.erase(i);
+ if (edge->use_console())
+ printer_.SetConsoleLocked(false);
+
if (config_.verbosity == BuildConfig::QUIET)
return;
- if (printer_.is_smart_terminal())
+ if (!edge->use_console() && printer_.is_smart_terminal())
PrintStatus(edge);
// Print the command that is spewing before printing its output.
@@ -145,6 +151,7 @@ void BuildStatus::BuildEdgeFinished(Edge* edge,
}
void BuildStatus::BuildFinished() {
+ printer_.SetConsoleLocked(false);
printer_.PrintOnNewLine("");
}
@@ -488,7 +495,7 @@ bool RealCommandRunner::CanRunMore() {
bool RealCommandRunner::StartCommand(Edge* edge) {
string command = edge->EvaluateCommand();
- Subprocess* subproc = subprocs_.Add(command);
+ Subprocess* subproc = subprocs_.Add(command, edge->use_console());
if (!subproc)
return false;
subproc_to_edge_.insert(make_pair(subproc, edge));
@@ -534,7 +541,7 @@ void Builder::Cleanup() {
for (vector<Edge*>::iterator i = active_edges.begin();
i != active_edges.end(); ++i) {
- string depfile = (*i)->GetBinding("depfile");
+ string depfile = (*i)->GetUnescapedDepfile();
for (vector<Node*>::iterator ni = (*i)->outputs_.begin();
ni != (*i)->outputs_.end(); ++ni) {
// Only delete this output if it was actually modified. This is
@@ -610,6 +617,7 @@ bool Builder::Build(string* err) {
if (failures_allowed && command_runner_->CanRunMore()) {
if (Edge* edge = plan_.FindWork()) {
if (!StartEdge(edge, err)) {
+ Cleanup();
status_->BuildFinished();
return false;
}
@@ -630,6 +638,7 @@ bool Builder::Build(string* err) {
CommandRunner::Result result;
if (!command_runner_->WaitForCommand(&result) ||
result.status == ExitInterrupted) {
+ Cleanup();
status_->BuildFinished();
*err = "interrupted by user";
return false;
@@ -637,6 +646,7 @@ bool Builder::Build(string* err) {
--pending_commands;
if (!FinishCommand(&result, err)) {
+ Cleanup();
status_->BuildFinished();
return false;
}
@@ -686,7 +696,7 @@ bool Builder::StartEdge(Edge* edge, string* err) {
// Create response file, if needed
// XXX: this may also block; do we care?
- string rspfile = edge->GetBinding("rspfile");
+ string rspfile = edge->GetUnescapedRspfile();
if (!rspfile.empty()) {
string content = edge->GetBinding("rspfile_content");
if (!disk_interface_->WriteFile(rspfile, content))
@@ -714,9 +724,11 @@ bool Builder::FinishCommand(CommandRunner::Result* result, string* err) {
// build perspective.
vector<Node*> deps_nodes;
string deps_type = edge->GetBinding("deps");
+ const string deps_prefix = edge->GetBinding("msvc_deps_prefix");
if (!deps_type.empty()) {
string extract_err;
- if (!ExtractDeps(result, deps_type, &deps_nodes, &extract_err) &&
+ if (!ExtractDeps(result, deps_type, deps_prefix, &deps_nodes,
+ &extract_err) &&
result->success()) {
if (!result->output.empty())
result->output.append("\n");
@@ -760,7 +772,7 @@ bool Builder::FinishCommand(CommandRunner::Result* result, string* err) {
restat_mtime = input_mtime;
}
- string depfile = edge->GetBinding("depfile");
+ string depfile = edge->GetUnescapedDepfile();
if (restat_mtime != 0 && deps_type.empty() && !depfile.empty()) {
TimeStamp depfile_mtime = disk_interface_->Stat(depfile);
if (depfile_mtime > restat_mtime)
@@ -776,7 +788,7 @@ bool Builder::FinishCommand(CommandRunner::Result* result, string* err) {
plan_.EdgeFinished(edge);
// Delete any left over response file.
- string rspfile = edge->GetBinding("rspfile");
+ string rspfile = edge->GetUnescapedRspfile();
if (!rspfile.empty() && !g_keep_rsp)
disk_interface_->RemoveFile(rspfile);
@@ -802,12 +814,13 @@ bool Builder::FinishCommand(CommandRunner::Result* result, string* err) {
bool Builder::ExtractDeps(CommandRunner::Result* result,
const string& deps_type,
+ const string& deps_prefix,
vector<Node*>* deps_nodes,
string* err) {
#ifdef _WIN32
if (deps_type == "msvc") {
CLParser parser;
- result->output = parser.Parse(result->output);
+ result->output = parser.Parse(result->output, deps_prefix);
for (set<string>::iterator i = parser.includes_.begin();
i != parser.includes_.end(); ++i) {
deps_nodes->push_back(state_->GetNode(*i));
@@ -815,7 +828,7 @@ bool Builder::ExtractDeps(CommandRunner::Result* result,
} else
#endif
if (deps_type == "gcc") {
- string depfile = result->edge->GetBinding("depfile");
+ string depfile = result->edge->GetUnescapedDepfile();
if (depfile.empty()) {
*err = string("edge with deps=gcc but no depfile makes no sense");
return false;
diff --git a/src/build.h b/src/build.h
index 5b6c83c..eb3636a 100644
--- a/src/build.h
+++ b/src/build.h
@@ -180,8 +180,9 @@ struct Builder {
BuildStatus* status_;
private:
- bool ExtractDeps(CommandRunner::Result* result, const string& deps_type,
- vector<Node*>* deps_nodes, string* err);
+ bool ExtractDeps(CommandRunner::Result* result, const string& deps_type,
+ const string& deps_prefix, vector<Node*>* deps_nodes,
+ string* err);
DiskInterface* disk_interface_;
DependencyScan scan_;
diff --git a/src/build_log.cc b/src/build_log.cc
index b92a06f..3f24c16 100644
--- a/src/build_log.cc
+++ b/src/build_log.cc
@@ -108,9 +108,10 @@ BuildLog::~BuildLog() {
Close();
}
-bool BuildLog::OpenForWrite(const string& path, string* err) {
+bool BuildLog::OpenForWrite(const string& path, const BuildLogUser& user,
+ string* err) {
if (needs_recompaction_) {
- if (!Recompact(path, err))
+ if (!Recompact(path, user, err))
return false;
}
@@ -350,7 +351,8 @@ bool BuildLog::WriteEntry(FILE* f, const LogEntry& entry) {
entry.output.c_str(), entry.command_hash) > 0;
}
-bool BuildLog::Recompact(const string& path, string* err) {
+bool BuildLog::Recompact(const string& path, const BuildLogUser& user,
+ string* err) {
METRIC_RECORD(".ninja_log recompact");
printf("Recompacting log...\n");
@@ -368,7 +370,13 @@ bool BuildLog::Recompact(const string& path, string* err) {
return false;
}
+ vector<StringPiece> dead_outputs;
for (Entries::iterator i = entries_.begin(); i != entries_.end(); ++i) {
+ if (user.IsPathDead(i->first)) {
+ dead_outputs.push_back(i->first);
+ continue;
+ }
+
if (!WriteEntry(f, *i->second)) {
*err = strerror(errno);
fclose(f);
@@ -376,6 +384,9 @@ bool BuildLog::Recompact(const string& path, string* err) {
}
}
+ for (size_t i = 0; i < dead_outputs.size(); ++i)
+ entries_.erase(dead_outputs[i]);
+
fclose(f);
if (unlink(path.c_str()) < 0) {
*err = strerror(errno);
diff --git a/src/build_log.h b/src/build_log.h
index eeac5b3..fe81a85 100644
--- a/src/build_log.h
+++ b/src/build_log.h
@@ -25,6 +25,13 @@ using namespace std;
struct Edge;
+/// Can answer questions about the manifest for the BuildLog.
+struct BuildLogUser {
+ /// Return if a given output no longer part of the build manifest.
+ /// This is only called during recompaction and doesn't have to be fast.
+ virtual bool IsPathDead(StringPiece s) const = 0;
+};
+
/// Store a log of every command ran for every build.
/// It has a few uses:
///
@@ -36,7 +43,7 @@ struct BuildLog {
BuildLog();
~BuildLog();
- bool OpenForWrite(const string& path, string* err);
+ bool OpenForWrite(const string& path, const BuildLogUser& user, string* err);
bool RecordCommand(Edge* edge, int start_time, int end_time,
TimeStamp restat_mtime = 0);
void Close();
@@ -72,7 +79,7 @@ struct BuildLog {
bool WriteEntry(FILE* f, const LogEntry& entry);
/// Rewrite the known log entries, throwing away old data.
- bool Recompact(const string& path, string* err);
+ bool Recompact(const string& path, const BuildLogUser& user, string* err);
typedef ExternalStringHashMap<LogEntry*>::Type Entries;
const Entries& entries() const { return entries_; }
diff --git a/src/build_log_perftest.cc b/src/build_log_perftest.cc
index a09beb8..810c065 100644
--- a/src/build_log_perftest.cc
+++ b/src/build_log_perftest.cc
@@ -28,10 +28,15 @@
const char kTestFilename[] = "BuildLogPerfTest-tempfile";
+struct NoDeadPaths : public BuildLogUser {
+ virtual bool IsPathDead(StringPiece) const { return false; }
+};
+
bool WriteTestData(string* err) {
BuildLog log;
- if (!log.OpenForWrite(kTestFilename, err))
+ NoDeadPaths no_dead_paths;
+ if (!log.OpenForWrite(kTestFilename, no_dead_paths, err))
return false;
/*
diff --git a/src/build_log_test.cc b/src/build_log_test.cc
index 4639bc9..6738c7b 100644
--- a/src/build_log_test.cc
+++ b/src/build_log_test.cc
@@ -30,7 +30,7 @@ namespace {
const char kTestFilename[] = "BuildLogTest-tempfile";
-struct BuildLogTest : public StateTestWithBuiltinRules {
+struct BuildLogTest : public StateTestWithBuiltinRules, public BuildLogUser {
virtual void SetUp() {
// In case a crashing test left a stale file behind.
unlink(kTestFilename);
@@ -38,6 +38,7 @@ struct BuildLogTest : public StateTestWithBuiltinRules {
virtual void TearDown() {
unlink(kTestFilename);
}
+ virtual bool IsPathDead(StringPiece s) const { return false; }
};
TEST_F(BuildLogTest, WriteRead) {
@@ -47,7 +48,7 @@ TEST_F(BuildLogTest, WriteRead) {
BuildLog log1;
string err;
- EXPECT_TRUE(log1.OpenForWrite(kTestFilename, &err));
+ EXPECT_TRUE(log1.OpenForWrite(kTestFilename, *this, &err));
ASSERT_EQ("", err);
log1.RecordCommand(state_.edges_[0], 15, 18);
log1.RecordCommand(state_.edges_[1], 20, 25);
@@ -75,7 +76,7 @@ TEST_F(BuildLogTest, FirstWriteAddsSignature) {
BuildLog log;
string contents, err;
- EXPECT_TRUE(log.OpenForWrite(kTestFilename, &err));
+ EXPECT_TRUE(log.OpenForWrite(kTestFilename, *this, &err));
ASSERT_EQ("", err);
log.Close();
@@ -86,7 +87,7 @@ TEST_F(BuildLogTest, FirstWriteAddsSignature) {
EXPECT_EQ(kExpectedVersion, contents);
// Opening the file anew shouldn't add a second version string.
- EXPECT_TRUE(log.OpenForWrite(kTestFilename, &err));
+ EXPECT_TRUE(log.OpenForWrite(kTestFilename, *this, &err));
ASSERT_EQ("", err);
log.Close();
@@ -122,7 +123,7 @@ TEST_F(BuildLogTest, Truncate) {
BuildLog log1;
string err;
- EXPECT_TRUE(log1.OpenForWrite(kTestFilename, &err));
+ EXPECT_TRUE(log1.OpenForWrite(kTestFilename, *this, &err));
ASSERT_EQ("", err);
log1.RecordCommand(state_.edges_[0], 15, 18);
log1.RecordCommand(state_.edges_[1], 20, 25);
@@ -137,7 +138,7 @@ TEST_F(BuildLogTest, Truncate) {
for (off_t size = statbuf.st_size; size > 0; --size) {
BuildLog log2;
string err;
- EXPECT_TRUE(log2.OpenForWrite(kTestFilename, &err));
+ EXPECT_TRUE(log2.OpenForWrite(kTestFilename, *this, &err));
ASSERT_EQ("", err);
log2.RecordCommand(state_.edges_[0], 15, 18);
log2.RecordCommand(state_.edges_[1], 20, 25);
@@ -261,4 +262,44 @@ TEST_F(BuildLogTest, MultiTargetEdge) {
ASSERT_EQ(22, e2->end_time);
}
+struct BuildLogRecompactTest : public BuildLogTest {
+ virtual bool IsPathDead(StringPiece s) const { return s == "out2"; }
+};
+
+TEST_F(BuildLogRecompactTest, Recompact) {
+ AssertParse(&state_,
+"build out: cat in\n"
+"build out2: cat in\n");
+
+ BuildLog log1;
+ string err;
+ EXPECT_TRUE(log1.OpenForWrite(kTestFilename, *this, &err));
+ ASSERT_EQ("", err);
+ // Record the same edge several times, to trigger recompaction
+ // the next time the log is opened.
+ for (int i = 0; i < 200; ++i)
+ log1.RecordCommand(state_.edges_[0], 15, 18 + i);
+ log1.RecordCommand(state_.edges_[1], 21, 22);
+ log1.Close();
+
+ // Load...
+ BuildLog log2;
+ EXPECT_TRUE(log2.Load(kTestFilename, &err));
+ ASSERT_EQ("", err);
+ ASSERT_EQ(2u, log2.entries().size());
+ ASSERT_TRUE(log2.LookupByOutput("out"));
+ ASSERT_TRUE(log2.LookupByOutput("out2"));
+ // ...and force a recompaction.
+ EXPECT_TRUE(log2.OpenForWrite(kTestFilename, *this, &err));
+ log2.Close();
+
+ // "out2" is dead, it should've been removed.
+ BuildLog log3;
+ EXPECT_TRUE(log2.Load(kTestFilename, &err));
+ ASSERT_EQ("", err);
+ ASSERT_EQ(1u, log2.entries().size());
+ ASSERT_TRUE(log2.LookupByOutput("out"));
+ ASSERT_FALSE(log2.LookupByOutput("out2"));
+}
+
} // anonymous namespace
diff --git a/src/build_test.cc b/src/build_test.cc
index e206cd8..dad69dc 100644
--- a/src/build_test.cc
+++ b/src/build_test.cc
@@ -44,6 +44,8 @@ struct PlanTest : public StateTestWithBuiltinRules {
ASSERT_FALSE(plan_.FindWork());
sort(ret->begin(), ret->end(), CompareEdgesByOutput::cmp);
}
+
+ void TestPoolWithDepthOne(const char *test_case);
};
TEST_F(PlanTest, Basic) {
@@ -197,15 +199,8 @@ TEST_F(PlanTest, DependencyCycle) {
ASSERT_EQ("dependency cycle: out -> mid -> in -> pre -> out", err);
}
-TEST_F(PlanTest, PoolWithDepthOne) {
- ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
-"pool foobar\n"
-" depth = 1\n"
-"rule poolcat\n"
-" command = cat $in > $out\n"
-" pool = foobar\n"
-"build out1: poolcat in\n"
-"build out2: poolcat in\n"));
+void PlanTest::TestPoolWithDepthOne(const char* test_case) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_, test_case));
GetNode("out1")->MarkDirty();
GetNode("out2")->MarkDirty();
string err;
@@ -239,6 +234,26 @@ TEST_F(PlanTest, PoolWithDepthOne) {
ASSERT_EQ(0, edge);
}
+TEST_F(PlanTest, PoolWithDepthOne) {
+ TestPoolWithDepthOne(
+"pool foobar\n"
+" depth = 1\n"
+"rule poolcat\n"
+" command = cat $in > $out\n"
+" pool = foobar\n"
+"build out1: poolcat in\n"
+"build out2: poolcat in\n");
+}
+
+TEST_F(PlanTest, ConsolePool) {
+ TestPoolWithDepthOne(
+"rule poolcat\n"
+" command = cat $in > $out\n"
+" pool = console\n"
+"build out1: poolcat in\n"
+"build out2: poolcat in\n");
+}
+
TEST_F(PlanTest, PoolsWithDepthTwo) {
ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
"pool foobar\n"
@@ -412,7 +427,7 @@ struct FakeCommandRunner : public CommandRunner {
VirtualFileSystem* fs_;
};
-struct BuildTest : public StateTestWithBuiltinRules {
+struct BuildTest : public StateTestWithBuiltinRules, public BuildLogUser {
BuildTest() : config_(MakeConfig()), command_runner_(&fs_),
builder_(&state_, config_, NULL, NULL, &fs_),
status_(config_) {
@@ -435,6 +450,8 @@ struct BuildTest : public StateTestWithBuiltinRules {
builder_.command_runner_.release();
}
+ virtual bool IsPathDead(StringPiece s) const { return false; }
+
/// Rebuild target in the 'working tree' (fs_).
/// State of command_runner_ and logs contents (if specified) ARE MODIFIED.
/// Handy to check for NOOP builds, and higher-level rebuild tests.
@@ -469,7 +486,7 @@ void BuildTest::RebuildTarget(const string& target, const char* manifest,
BuildLog build_log, *pbuild_log = NULL;
if (log_path) {
ASSERT_TRUE(build_log.Load(log_path, &err));
- ASSERT_TRUE(build_log.OpenForWrite(log_path, &err));
+ ASSERT_TRUE(build_log.OpenForWrite(log_path, *this, &err));
ASSERT_EQ("", err);
pbuild_log = &build_log;
}
@@ -504,6 +521,7 @@ bool FakeCommandRunner::StartCommand(Edge* edge) {
commands_ran_.push_back(edge->EvaluateCommand());
if (edge->rule().name() == "cat" ||
edge->rule().name() == "cat_rsp" ||
+ edge->rule().name() == "cat_rsp_out" ||
edge->rule().name() == "cc" ||
edge->rule().name() == "touch" ||
edge->rule().name() == "touch-interrupt") {
@@ -513,7 +531,8 @@ bool FakeCommandRunner::StartCommand(Edge* edge) {
}
} else if (edge->rule().name() == "true" ||
edge->rule().name() == "fail" ||
- edge->rule().name() == "interrupt") {
+ edge->rule().name() == "interrupt" ||
+ edge->rule().name() == "console") {
// Don't do anything.
} else {
printf("unknown command\n");
@@ -537,6 +556,15 @@ bool FakeCommandRunner::WaitForCommand(Result* result) {
return true;
}
+ if (edge->rule().name() == "console") {
+ if (edge->use_console())
+ result->status = ExitSuccess;
+ else
+ result->status = ExitFailure;
+ last_command_ = NULL;
+ return true;
+ }
+
if (edge->rule().name() == "fail")
result->status = ExitFailure;
else
@@ -748,13 +776,13 @@ TEST_F(BuildTest, DepFileMissing) {
string err;
ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
"rule cc\n command = cc $in\n depfile = $out.d\n"
-"build foo.o: cc foo.c\n"));
+"build fo$ o.o: cc foo.c\n"));
fs_.Create("foo.c", "");
- EXPECT_TRUE(builder_.AddTarget("foo.o", &err));
+ EXPECT_TRUE(builder_.AddTarget("fo o.o", &err));
ASSERT_EQ("", err);
ASSERT_EQ(1u, fs_.files_read_.size());
- EXPECT_EQ("foo.o.d", fs_.files_read_[0]);
+ EXPECT_EQ("fo o.o.d", fs_.files_read_[0]);
}
TEST_F(BuildTest, DepFileOK) {
@@ -1275,14 +1303,20 @@ TEST_F(BuildTest, RspFileSuccess)
" command = cat $rspfile > $out\n"
" rspfile = $rspfile\n"
" rspfile_content = $long_command\n"
+ "rule cat_rsp_out\n"
+ " command = cat $rspfile > $out\n"
+ " rspfile = $out.rsp\n"
+ " rspfile_content = $long_command\n"
"build out1: cat in\n"
"build out2: cat_rsp in\n"
- " rspfile = out2.rsp\n"
+ " rspfile = out 2.rsp\n"
+ " long_command = Some very long command\n"
+ "build out$ 3: cat_rsp_out in\n"
" long_command = Some very long command\n"));
fs_.Create("out1", "");
fs_.Create("out2", "");
- fs_.Create("out3", "");
+ fs_.Create("out 3", "");
fs_.Tick();
@@ -1293,20 +1327,24 @@ TEST_F(BuildTest, RspFileSuccess)
ASSERT_EQ("", err);
EXPECT_TRUE(builder_.AddTarget("out2", &err));
ASSERT_EQ("", err);
+ EXPECT_TRUE(builder_.AddTarget("out 3", &err));
+ ASSERT_EQ("", err);
size_t files_created = fs_.files_created_.size();
size_t files_removed = fs_.files_removed_.size();
EXPECT_TRUE(builder_.Build(&err));
- ASSERT_EQ(2u, command_runner_.commands_ran_.size()); // cat + cat_rsp
+ ASSERT_EQ(3u, command_runner_.commands_ran_.size());
- // The RSP file was created
- ASSERT_EQ(files_created + 1, fs_.files_created_.size());
- ASSERT_EQ(1u, fs_.files_created_.count("out2.rsp"));
+ // The RSP files were created
+ ASSERT_EQ(files_created + 2, fs_.files_created_.size());
+ ASSERT_EQ(1u, fs_.files_created_.count("out 2.rsp"));
+ ASSERT_EQ(1u, fs_.files_created_.count("out 3.rsp"));
- // The RSP file was removed
- ASSERT_EQ(files_removed + 1, fs_.files_removed_.size());
- ASSERT_EQ(1u, fs_.files_removed_.count("out2.rsp"));
+ // The RSP files were removed
+ ASSERT_EQ(files_removed + 2, fs_.files_removed_.size());
+ ASSERT_EQ(1u, fs_.files_removed_.count("out 2.rsp"));
+ ASSERT_EQ(1u, fs_.files_removed_.count("out 3.rsp"));
}
// Test that RSP file is created but not removed for commands, which fail
@@ -1777,7 +1815,7 @@ TEST_F(BuildWithDepsLogTest, DepFileOKDepsLog) {
string err;
const char* manifest =
"rule cc\n command = cc $in\n depfile = $out.d\n deps = gcc\n"
- "build foo.o: cc foo.c\n";
+ "build fo$ o.o: cc foo.c\n";
fs_.Create("foo.c", "");
@@ -1792,9 +1830,9 @@ TEST_F(BuildWithDepsLogTest, DepFileOKDepsLog) {
Builder builder(&state, config_, NULL, &deps_log, &fs_);
builder.command_runner_.reset(&command_runner_);
- EXPECT_TRUE(builder.AddTarget("foo.o", &err));
+ EXPECT_TRUE(builder.AddTarget("fo o.o", &err));
ASSERT_EQ("", err);
- fs_.Create("foo.o.d", "foo.o: blah.h bar.h\n");
+ fs_.Create("fo o.o.d", "fo\\ o.o: blah.h bar.h\n");
EXPECT_TRUE(builder.Build(&err));
EXPECT_EQ("", err);
@@ -1817,10 +1855,10 @@ TEST_F(BuildWithDepsLogTest, DepFileOKDepsLog) {
Edge* edge = state.edges_.back();
state.GetNode("bar.h")->MarkDirty(); // Mark bar.h as missing.
- EXPECT_TRUE(builder.AddTarget("foo.o", &err));
+ EXPECT_TRUE(builder.AddTarget("fo o.o", &err));
ASSERT_EQ("", err);
- // Expect three new edges: one generating foo.o, and two more from
+ // Expect three new edges: one generating fo o.o, and two more from
// loading the depfile.
ASSERT_EQ(3u, state.edges_.size());
// Expect our edge to now have three inputs: foo.c and two headers.
@@ -1909,3 +1947,20 @@ TEST_F(BuildWithDepsLogTest, RestatMissingDepfileDepslog) {
RebuildTarget("out", manifest, "build_log", "ninja_deps2");
ASSERT_EQ(0u, command_runner_.commands_ran_.size());
}
+
+TEST_F(BuildTest, Console) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule console\n"
+" command = console\n"
+" pool = console\n"
+"build cons: console in.txt\n"));
+
+ fs_.Create("in.txt", "");
+
+ string err;
+ EXPECT_TRUE(builder_.AddTarget("cons", &err));
+ ASSERT_EQ("", err);
+ EXPECT_TRUE(builder_.Build(&err));
+ EXPECT_EQ("", err);
+ ASSERT_EQ(1u, command_runner_.commands_ran_.size());
+}
diff --git a/src/clean.cc b/src/clean.cc
index 5d1974e..98c638c 100644
--- a/src/clean.cc
+++ b/src/clean.cc
@@ -80,11 +80,11 @@ bool Cleaner::IsAlreadyRemoved(const string& path) {
}
void Cleaner::RemoveEdgeFiles(Edge* edge) {
- string depfile = edge->GetBinding("depfile");
+ string depfile = edge->GetUnescapedDepfile();
if (!depfile.empty())
Remove(depfile);
- string rspfile = edge->GetBinding("rspfile");
+ string rspfile = edge->GetUnescapedRspfile();
if (!rspfile.empty())
Remove(rspfile);
}
diff --git a/src/clean_test.cc b/src/clean_test.cc
index 04cff73..5869bbb 100644
--- a/src/clean_test.cc
+++ b/src/clean_test.cc
@@ -286,8 +286,7 @@ TEST_F(CleanTest, CleanRspFile) {
" rspfile = $rspfile\n"
" rspfile_content=$in\n"
"build out1: cc in1\n"
-" rspfile = cc1.rsp\n"
-" rspfile_content=$in\n"));
+" rspfile = cc1.rsp\n"));
fs_.Create("out1", "");
fs_.Create("cc1.rsp", "");
@@ -307,10 +306,9 @@ TEST_F(CleanTest, CleanRsp) {
"build out1: cat in1\n"
"build in2: cat_rsp src2\n"
" rspfile=in2.rsp\n"
-" rspfile_content=$in\n"
"build out2: cat_rsp in2\n"
" rspfile=out2.rsp\n"
-" rspfile_content=$in\n"));
+));
fs_.Create("in1", "");
fs_.Create("out1", "");
fs_.Create("in2.rsp", "");
@@ -336,8 +334,6 @@ TEST_F(CleanTest, CleanRsp) {
EXPECT_EQ(0, fs_.Stat("out2"));
EXPECT_EQ(0, fs_.Stat("in2.rsp"));
EXPECT_EQ(0, fs_.Stat("out2.rsp"));
-
- fs_.files_removed_.clear();
}
TEST_F(CleanTest, CleanFailure) {
@@ -372,3 +368,31 @@ TEST_F(CleanTest, CleanPhony) {
EXPECT_EQ(2, cleaner.cleaned_files_count());
EXPECT_NE(0, fs_.Stat("phony"));
}
+
+TEST_F(CleanTest, CleanDepFileAndRspFileWithSpaces) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule cc_dep\n"
+" command = cc $in > $out\n"
+" depfile = $out.d\n"
+"rule cc_rsp\n"
+" command = cc $in > $out\n"
+" rspfile = $out.rsp\n"
+" rspfile_content = $in\n"
+"build out$ 1: cc_dep in$ 1\n"
+"build out$ 2: cc_rsp in$ 1\n"
+));
+ fs_.Create("out 1", "");
+ fs_.Create("out 2", "");
+ fs_.Create("out 1.d", "");
+ fs_.Create("out 2.rsp", "");
+
+ Cleaner cleaner(&state_, config_, &fs_);
+ EXPECT_EQ(0, cleaner.CleanAll());
+ EXPECT_EQ(4, cleaner.cleaned_files_count());
+ EXPECT_EQ(4u, fs_.files_removed_.size());
+
+ EXPECT_EQ(0, fs_.Stat("out 1"));
+ EXPECT_EQ(0, fs_.Stat("out 2"));
+ EXPECT_EQ(0, fs_.Stat("out 1.d"));
+ EXPECT_EQ(0, fs_.Stat("out 2.rsp"));
+}
diff --git a/src/debug_flags.cc b/src/debug_flags.cc
index 75f1ea5..8065001 100644
--- a/src/debug_flags.cc
+++ b/src/debug_flags.cc
@@ -15,3 +15,5 @@
bool g_explaining = false;
bool g_keep_rsp = false;
+
+bool g_experimental_statcache = true;
diff --git a/src/debug_flags.h b/src/debug_flags.h
index ba3ebf3..7965585 100644
--- a/src/debug_flags.h
+++ b/src/debug_flags.h
@@ -26,4 +26,6 @@ extern bool g_explaining;
extern bool g_keep_rsp;
+extern bool g_experimental_statcache;
+
#endif // NINJA_EXPLAIN_H_
diff --git a/src/depfile_parser.cc b/src/depfile_parser.cc
index 5a30c6b..4ca3943 100644
--- a/src/depfile_parser.cc
+++ b/src/depfile_parser.cc
@@ -53,10 +53,10 @@ bool DepfileParser::Parse(string* content, string* err) {
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
- 0, 128, 128, 128, 128, 128, 128, 128,
- 128, 128, 128, 128, 128, 128, 128, 128,
+ 0, 128, 0, 0, 0, 0, 0, 0,
+ 128, 128, 0, 128, 128, 128, 128, 128,
128, 128, 128, 128, 128, 128, 128, 128,
- 128, 128, 128, 128, 128, 128, 0, 0,
+ 128, 128, 128, 0, 0, 128, 0, 0,
128, 128, 128, 128, 128, 128, 128, 128,
128, 128, 128, 128, 128, 128, 128, 128,
128, 128, 128, 128, 128, 128, 128, 128,
@@ -64,7 +64,7 @@ bool DepfileParser::Parse(string* content, string* err) {
0, 128, 128, 128, 128, 128, 128, 128,
128, 128, 128, 128, 128, 128, 128, 128,
128, 128, 128, 128, 128, 128, 128, 128,
- 128, 128, 128, 0, 0, 0, 128, 0,
+ 128, 128, 128, 128, 0, 128, 128, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
@@ -84,42 +84,59 @@ bool DepfileParser::Parse(string* content, string* err) {
};
yych = *in;
- if (yych <= '[') {
+ if (yych <= '=') {
if (yych <= '$') {
- if (yych <= 0x00) goto yy7;
- if (yych <= ' ') goto yy9;
- if (yych <= '#') goto yy6;
- goto yy4;
+ if (yych <= ' ') {
+ if (yych <= 0x00) goto yy7;
+ goto yy9;
+ } else {
+ if (yych <= '!') goto yy5;
+ if (yych <= '#') goto yy9;
+ goto yy4;
+ }
} else {
- if (yych <= '=') goto yy6;
- if (yych <= '?') goto yy9;
- if (yych <= 'Z') goto yy6;
- goto yy9;
+ if (yych <= '*') {
+ if (yych <= '\'') goto yy9;
+ if (yych <= ')') goto yy5;
+ goto yy9;
+ } else {
+ if (yych <= ':') goto yy5;
+ if (yych <= '<') goto yy9;
+ goto yy5;
+ }
}
} else {
- if (yych <= '`') {
- if (yych <= '\\') goto yy2;
- if (yych == '_') goto yy6;
- goto yy9;
+ if (yych <= '^') {
+ if (yych <= 'Z') {
+ if (yych <= '?') goto yy9;
+ goto yy5;
+ } else {
+ if (yych != '\\') goto yy9;
+ }
} else {
- if (yych <= 'z') goto yy6;
- if (yych == '~') goto yy6;
- goto yy9;
+ if (yych <= '{') {
+ if (yych == '`') goto yy9;
+ goto yy5;
+ } else {
+ if (yych <= '|') goto yy9;
+ if (yych <= '~') goto yy5;
+ goto yy9;
+ }
}
}
-yy2:
++in;
- if ((yych = *in) <= '#') {
- if (yych <= '\n') {
+ if ((yych = *in) <= '"') {
+ if (yych <= '\f') {
if (yych <= 0x00) goto yy3;
- if (yych <= '\t') goto yy14;
+ if (yych != '\n') goto yy14;
} else {
+ if (yych <= '\r') goto yy3;
if (yych == ' ') goto yy16;
- if (yych <= '"') goto yy14;
- goto yy16;
+ goto yy14;
}
} else {
if (yych <= 'Z') {
+ if (yych <= '#') goto yy16;
if (yych == '*') goto yy16;
goto yy14;
} else {
@@ -135,10 +152,14 @@ yy3:
break;
}
yy4:
+ yych = *++in;
+ if (yych == '$') goto yy12;
+ goto yy3;
+yy5:
++in;
- if ((yych = *in) == '$') goto yy12;
+ yych = *in;
goto yy11;
-yy5:
+yy6:
{
// Got a span of plain text.
int len = (int)(in - start);
@@ -148,9 +169,6 @@ yy5:
out += len;
continue;
}
-yy6:
- yych = *++in;
- goto yy11;
yy7:
++in;
{
@@ -166,12 +184,9 @@ yy11:
if (yybm[0+yych] & 128) {
goto yy10;
}
- goto yy5;
+ goto yy6;
yy12:
++in;
- if (yybm[0+(yych = *in)] & 128) {
- goto yy10;
- }
{
// De-escape dollar character.
*out++ = '$';
@@ -211,7 +226,7 @@ yy16:
} else if (!out_.str_) {
out_ = StringPiece(filename, len);
} else if (out_ != StringPiece(filename, len)) {
- *err = "depfile has multiple output paths.";
+ *err = "depfile has multiple output paths";
return false;
}
}
diff --git a/src/depfile_parser.in.cc b/src/depfile_parser.in.cc
index cf24a09..b59baf0 100644
--- a/src/depfile_parser.in.cc
+++ b/src/depfile_parser.in.cc
@@ -67,13 +67,13 @@ bool DepfileParser::Parse(string* content, string* err) {
*out++ = '$';
continue;
}
- '\\' [^\000\n] {
+ '\\' [^\000\r\n] {
// Let backslash before other characters through verbatim.
*out++ = '\\';
*out++ = yych;
continue;
}
- [a-zA-Z0-9+,/_:.~()@=-!]+ {
+ [a-zA-Z0-9+,/_:.~()}{@=!-]+ {
// Got a span of plain text.
int len = (int)(in - start);
// Need to shift it over if we're overwriting backslashes.
@@ -108,7 +108,7 @@ bool DepfileParser::Parse(string* content, string* err) {
} else if (!out_.str_) {
out_ = StringPiece(filename, len);
} else if (out_ != StringPiece(filename, len)) {
- *err = "depfile has multiple output paths.";
+ *err = "depfile has multiple output paths";
return false;
}
}
diff --git a/src/parser_perftest.cc b/src/depfile_parser_perftest.cc
index b215221..b215221 100644
--- a/src/parser_perftest.cc
+++ b/src/depfile_parser_perftest.cc
diff --git a/src/depfile_parser_test.cc b/src/depfile_parser_test.cc
index 0f6771a..a5f3321 100644
--- a/src/depfile_parser_test.cc
+++ b/src/depfile_parser_test.cc
@@ -58,6 +58,17 @@ TEST_F(DepfileParserTest, Continuation) {
EXPECT_EQ(2u, parser_.ins_.size());
}
+TEST_F(DepfileParserTest, CarriageReturnContinuation) {
+ string err;
+ EXPECT_TRUE(Parse(
+"foo.o: \\\r\n"
+" bar.h baz.h\r\n",
+ &err));
+ ASSERT_EQ("", err);
+ EXPECT_EQ("foo.o", parser_.out_.AsString());
+ EXPECT_EQ(2u, parser_.ins_.size());
+}
+
TEST_F(DepfileParserTest, BackSlashes) {
string err;
EXPECT_TRUE(Parse(
@@ -109,16 +120,19 @@ TEST_F(DepfileParserTest, SpecialChars) {
string err;
EXPECT_TRUE(Parse(
"C:/Program\\ Files\\ (x86)/Microsoft\\ crtdefs.h: \n"
-" en@quot.header~ t+t-x!=1",
+" en@quot.header~ t+t-x!=1 \n"
+" openldap/slapd.d/cn=config/cn=schema/cn={0}core.ldif",
&err));
ASSERT_EQ("", err);
EXPECT_EQ("C:/Program Files (x86)/Microsoft crtdefs.h",
parser_.out_.AsString());
- ASSERT_EQ(2u, parser_.ins_.size());
+ ASSERT_EQ(3u, parser_.ins_.size());
EXPECT_EQ("en@quot.header~",
parser_.ins_[0].AsString());
EXPECT_EQ("t+t-x!=1",
parser_.ins_[1].AsString());
+ EXPECT_EQ("openldap/slapd.d/cn=config/cn=schema/cn={0}core.ldif",
+ parser_.ins_[2].AsString());
}
TEST_F(DepfileParserTest, UnifyMultipleOutputs) {
@@ -136,4 +150,5 @@ TEST_F(DepfileParserTest, RejectMultipleDifferentOutputs) {
// check that multiple different outputs are rejected by the parser
string err;
EXPECT_FALSE(Parse("foo bar: x y z", &err));
+ ASSERT_EQ("depfile has multiple output paths", err);
}
diff --git a/src/deps_log.cc b/src/deps_log.cc
index 4f1214a..61df387 100644
--- a/src/deps_log.cc
+++ b/src/deps_log.cc
@@ -325,6 +325,9 @@ bool DepsLog::Recompact(const string& path, string* err) {
Deps* deps = deps_[old_id];
if (!deps) continue; // If nodes_[old_id] is a leaf, it has no deps.
+ if (!IsDepsEntryLiveFor(nodes_[old_id]))
+ continue;
+
if (!new_log.RecordDeps(nodes_[old_id], deps->mtime,
deps->node_count, deps->nodes)) {
new_log.Close();
@@ -351,6 +354,16 @@ bool DepsLog::Recompact(const string& path, string* err) {
return true;
}
+bool DepsLog::IsDepsEntryLiveFor(Node* node) {
+ // Skip entries that don't have in-edges or whose edges don't have a
+ // "deps" attribute. They were in the deps log from previous builds, but
+ // the the files they were for were removed from the build and their deps
+ // entries are no longer needed.
+ // (Without the check for "deps", a chain of two or more nodes that each
+ // had deps wouldn't be collected in a single recompaction.)
+ return node->in_edge() && !node->in_edge()->GetBinding("deps").empty();
+}
+
bool DepsLog::UpdateDeps(int out_id, Deps* deps) {
if (out_id >= (int)deps_.size())
deps_.resize(out_id + 1);
diff --git a/src/deps_log.h b/src/deps_log.h
index babf828..cec0257 100644
--- a/src/deps_log.h
+++ b/src/deps_log.h
@@ -88,6 +88,14 @@ struct DepsLog {
/// Rewrite the known log entries, throwing away old data.
bool Recompact(const string& path, string* err);
+ /// Returns if the deps entry for a node is still reachable from the manifest.
+ ///
+ /// The deps log can contain deps entries for files that were built in the
+ /// past but are no longer part of the manifest. This function returns if
+ /// this is the case for a given node. This function is slow, don't call
+ /// it from code that runs on every build.
+ bool IsDepsEntryLiveFor(Node* node);
+
/// Used for tests.
const vector<Node*>& nodes() const { return nodes_; }
const vector<Deps*>& deps() const { return deps_; }
diff --git a/src/deps_log_test.cc b/src/deps_log_test.cc
index 4e6cbac..e8e5138 100644
--- a/src/deps_log_test.cc
+++ b/src/deps_log_test.cc
@@ -163,10 +163,18 @@ TEST_F(DepsLogTest, DoubleEntry) {
// Verify that adding the new deps works and can be compacted away.
TEST_F(DepsLogTest, Recompact) {
+ const char kManifest[] =
+"rule cc\n"
+" command = cc\n"
+" deps = gcc\n"
+"build out.o: cc\n"
+"build other_out.o: cc\n";
+
// Write some deps to the file and grab its size.
int file_size;
{
State state;
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state, kManifest));
DepsLog log;
string err;
ASSERT_TRUE(log.OpenForWrite(kTestFilename, &err));
@@ -194,6 +202,7 @@ TEST_F(DepsLogTest, Recompact) {
int file_size_2;
{
State state;
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state, kManifest));
DepsLog log;
string err;
ASSERT_TRUE(log.Load(kTestFilename, &state, &err));
@@ -215,8 +224,10 @@ TEST_F(DepsLogTest, Recompact) {
// Now reload the file, verify the new deps have replaced the old, then
// recompact.
+ int file_size_3;
{
State state;
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state, kManifest));
DepsLog log;
string err;
ASSERT_TRUE(log.Load(kTestFilename, &state, &err));
@@ -257,9 +268,53 @@ TEST_F(DepsLogTest, Recompact) {
// The file should have shrunk a bit for the smaller deps.
struct stat st;
ASSERT_EQ(0, stat(kTestFilename, &st));
- int file_size_3 = (int)st.st_size;
+ file_size_3 = (int)st.st_size;
ASSERT_LT(file_size_3, file_size_2);
}
+
+ // Now reload the file and recompact with an empty manifest. The previous
+ // entries should be removed.
+ {
+ State state;
+ // Intentionally not parsing kManifest here.
+ DepsLog log;
+ string err;
+ ASSERT_TRUE(log.Load(kTestFilename, &state, &err));
+
+ Node* out = state.GetNode("out.o");
+ DepsLog::Deps* deps = log.GetDeps(out);
+ ASSERT_TRUE(deps);
+ ASSERT_EQ(1, deps->mtime);
+ ASSERT_EQ(1, deps->node_count);
+ ASSERT_EQ("foo.h", deps->nodes[0]->path());
+
+ Node* other_out = state.GetNode("other_out.o");
+ deps = log.GetDeps(other_out);
+ ASSERT_TRUE(deps);
+ ASSERT_EQ(1, deps->mtime);
+ ASSERT_EQ(2, deps->node_count);
+ ASSERT_EQ("foo.h", deps->nodes[0]->path());
+ ASSERT_EQ("baz.h", deps->nodes[1]->path());
+
+ ASSERT_TRUE(log.Recompact(kTestFilename, &err));
+
+ // The previous entries should have been removed.
+ deps = log.GetDeps(out);
+ ASSERT_FALSE(deps);
+
+ deps = log.GetDeps(other_out);
+ ASSERT_FALSE(deps);
+
+ // The .h files pulled in via deps should no longer have ids either.
+ ASSERT_EQ(-1, state.LookupNode("foo.h")->id());
+ ASSERT_EQ(-1, state.LookupNode("baz.h")->id());
+
+ // The file should have shrunk more.
+ struct stat st;
+ ASSERT_EQ(0, stat(kTestFilename, &st));
+ int file_size_4 = (int)st.st_size;
+ ASSERT_LT(file_size_4, file_size_3);
+ }
}
// Verify that invalid file headers cause a new build.
diff --git a/src/disk_interface.cc b/src/disk_interface.cc
index 3233144..ae2146e 100644
--- a/src/disk_interface.cc
+++ b/src/disk_interface.cc
@@ -14,6 +14,8 @@
#include "disk_interface.h"
+#include <algorithm>
+
#include <errno.h>
#include <stdio.h>
#include <string.h>
@@ -31,15 +33,16 @@ namespace {
string DirName(const string& path) {
#ifdef _WIN32
- const char kPathSeparator = '\\';
+ const char kPathSeparators[] = "\\/";
#else
- const char kPathSeparator = '/';
+ const char kPathSeparators[] = "/";
#endif
-
- string::size_type slash_pos = path.rfind(kPathSeparator);
+ string::size_type slash_pos = path.find_last_of(kPathSeparators);
if (slash_pos == string::npos)
return string(); // Nothing to do.
- while (slash_pos > 0 && path[slash_pos - 1] == kPathSeparator)
+ const char* const kEnd = kPathSeparators + strlen(kPathSeparators);
+ while (slash_pos > 0 &&
+ std::find(kPathSeparators, kEnd, path[slash_pos - 1]) != kEnd)
--slash_pos;
return path.substr(0, slash_pos);
}
@@ -52,6 +55,80 @@ int MakeDir(const string& path) {
#endif
}
+#ifdef _WIN32
+TimeStamp TimeStampFromFileTime(const FILETIME& filetime) {
+ // FILETIME is in 100-nanosecond increments since the Windows epoch.
+ // We don't much care about epoch correctness but we do want the
+ // resulting value to fit in an integer.
+ uint64_t mtime = ((uint64_t)filetime.dwHighDateTime << 32) |
+ ((uint64_t)filetime.dwLowDateTime);
+ mtime /= 1000000000LL / 100; // 100ns -> s.
+ mtime -= 12622770400LL; // 1600 epoch -> 2000 epoch (subtract 400 years).
+ return (TimeStamp)mtime;
+}
+
+TimeStamp StatSingleFile(const string& path, bool quiet) {
+ WIN32_FILE_ATTRIBUTE_DATA attrs;
+ if (!GetFileAttributesEx(path.c_str(), GetFileExInfoStandard, &attrs)) {
+ DWORD err = GetLastError();
+ if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND)
+ return 0;
+ if (!quiet) {
+ Error("GetFileAttributesEx(%s): %s", path.c_str(),
+ GetLastErrorString().c_str());
+ }
+ return -1;
+ }
+ return TimeStampFromFileTime(attrs.ftLastWriteTime);
+}
+
+#pragma warning(push)
+#pragma warning(disable: 4996) // GetVersionExA is deprecated post SDK 8.1.
+bool IsWindows7OrLater() {
+ OSVERSIONINFO version_info = { sizeof(version_info) };
+ if (!GetVersionEx(&version_info))
+ Fatal("GetVersionEx: %s", GetLastErrorString().c_str());
+ return version_info.dwMajorVersion > 6 ||
+ version_info.dwMajorVersion == 6 && version_info.dwMinorVersion >= 1;
+}
+#pragma warning(pop)
+
+bool StatAllFilesInDir(const string& dir, map<string, TimeStamp>* stamps,
+ bool quiet) {
+ // FindExInfoBasic is 30% faster than FindExInfoStandard.
+ static bool can_use_basic_info = IsWindows7OrLater();
+ // This is not in earlier SDKs.
+ const FINDEX_INFO_LEVELS kFindExInfoBasic =
+ static_cast<FINDEX_INFO_LEVELS>(1);
+ FINDEX_INFO_LEVELS level =
+ can_use_basic_info ? kFindExInfoBasic : FindExInfoStandard;
+ WIN32_FIND_DATAA ffd;
+ HANDLE find_handle = FindFirstFileExA((dir + "\\*").c_str(), level, &ffd,
+ FindExSearchNameMatch, NULL, 0);
+
+ if (find_handle == INVALID_HANDLE_VALUE) {
+ DWORD err = GetLastError();
+ if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND)
+ return true;
+ if (!quiet) {
+ Error("FindFirstFileExA(%s): %s", dir.c_str(),
+ GetLastErrorString().c_str());
+ }
+ return false;
+ }
+ do {
+ if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
+ continue;
+ string lowername = ffd.cFileName;
+ transform(lowername.begin(), lowername.end(), lowername.begin(), ::tolower);
+ stamps->insert(make_pair(lowername,
+ TimeStampFromFileTime(ffd.ftLastWriteTime)));
+ } while (FindNextFileA(find_handle, &ffd));
+ FindClose(find_handle);
+ return true;
+}
+#endif // _WIN32
+
} // namespace
// DiskInterface ---------------------------------------------------------------
@@ -75,7 +152,7 @@ bool DiskInterface::MakeDirs(const string& path) {
// RealDiskInterface -----------------------------------------------------------
-TimeStamp RealDiskInterface::Stat(const string& path) {
+TimeStamp RealDiskInterface::Stat(const string& path) const {
#ifdef _WIN32
// MSDN: "Naming Files, Paths, and Namespaces"
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
@@ -86,26 +163,25 @@ TimeStamp RealDiskInterface::Stat(const string& path) {
}
return -1;
}
- WIN32_FILE_ATTRIBUTE_DATA attrs;
- if (!GetFileAttributesEx(path.c_str(), GetFileExInfoStandard, &attrs)) {
- DWORD err = GetLastError();
- if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND)
- return 0;
- if (!quiet_) {
- Error("GetFileAttributesEx(%s): %s", path.c_str(),
- GetLastErrorString().c_str());
+ if (!use_cache_)
+ return StatSingleFile(path, quiet_);
+
+ string dir = DirName(path);
+ string base(path.substr(dir.size() ? dir.size() + 1 : 0));
+
+ transform(dir.begin(), dir.end(), dir.begin(), ::tolower);
+ transform(base.begin(), base.end(), base.begin(), ::tolower);
+
+ Cache::iterator ci = cache_.find(dir);
+ if (ci == cache_.end()) {
+ ci = cache_.insert(make_pair(dir, DirCache())).first;
+ if (!StatAllFilesInDir(dir.empty() ? "." : dir, &ci->second, quiet_)) {
+ cache_.erase(ci);
+ return -1;
}
- return -1;
}
- const FILETIME& filetime = attrs.ftLastWriteTime;
- // FILETIME is in 100-nanosecond increments since the Windows epoch.
- // We don't much care about epoch correctness but we do want the
- // resulting value to fit in an integer.
- uint64_t mtime = ((uint64_t)filetime.dwHighDateTime << 32) |
- ((uint64_t)filetime.dwLowDateTime);
- mtime /= 1000000000LL / 100; // 100ns -> s.
- mtime -= 12622770400LL; // 1600 epoch -> 2000 epoch (subtract 400 years).
- return (TimeStamp)mtime;
+ DirCache::iterator di = ci->second.find(base);
+ return di != ci->second.end() ? di->second : 0;
#else
struct stat st;
if (stat(path.c_str(), &st) < 0) {
@@ -146,6 +222,9 @@ bool RealDiskInterface::WriteFile(const string& path, const string& contents) {
bool RealDiskInterface::MakeDir(const string& path) {
if (::MakeDir(path) < 0) {
+ if (errno == EEXIST) {
+ return true;
+ }
Error("mkdir(%s): %s", path.c_str(), strerror(errno));
return false;
}
@@ -175,3 +254,11 @@ int RealDiskInterface::RemoveFile(const string& path) {
return 0;
}
}
+
+void RealDiskInterface::AllowStatCache(bool allow) {
+#ifdef _WIN32
+ use_cache_ = allow;
+ if (!use_cache_)
+ cache_.clear();
+#endif
+}
diff --git a/src/disk_interface.h b/src/disk_interface.h
index ff1e21c..a13bced 100644
--- a/src/disk_interface.h
+++ b/src/disk_interface.h
@@ -15,6 +15,7 @@
#ifndef NINJA_DISK_INTERFACE_H_
#define NINJA_DISK_INTERFACE_H_
+#include <map>
#include <string>
using namespace std;
@@ -29,7 +30,7 @@ struct DiskInterface {
/// stat() a file, returning the mtime, or 0 if missing and -1 on
/// other errors.
- virtual TimeStamp Stat(const string& path) = 0;
+ virtual TimeStamp Stat(const string& path) const = 0;
/// Create a directory, returning false on failure.
virtual bool MakeDir(const string& path) = 0;
@@ -55,9 +56,13 @@ struct DiskInterface {
/// Implementation of DiskInterface that actually hits the disk.
struct RealDiskInterface : public DiskInterface {
- RealDiskInterface() : quiet_(false) {}
+ RealDiskInterface() : quiet_(false)
+#ifdef _WIN32
+ , use_cache_(false)
+#endif
+ {}
virtual ~RealDiskInterface() {}
- virtual TimeStamp Stat(const string& path);
+ virtual TimeStamp Stat(const string& path) const;
virtual bool MakeDir(const string& path);
virtual bool WriteFile(const string& path, const string& contents);
virtual string ReadFile(const string& path, string* err);
@@ -65,6 +70,21 @@ struct RealDiskInterface : public DiskInterface {
/// Whether to print on errors. Used to make a test quieter.
bool quiet_;
+
+ /// Whether stat information can be cached. Only has an effect on Windows.
+ void AllowStatCache(bool allow);
+
+ private:
+#ifdef _WIN32
+ /// Whether stat information can be cached.
+ bool use_cache_;
+
+ typedef map<string, TimeStamp> DirCache;
+ // TODO: Neither a map nor a hashmap seems ideal here. If the statcache
+ // works out, come up with a better data structure.
+ typedef map<string, DirCache> Cache;
+ mutable Cache cache_;
+#endif
};
#endif // NINJA_DISK_INTERFACE_H_
diff --git a/src/disk_interface_test.cc b/src/disk_interface_test.cc
index 55822a6..f4e0bb0 100644
--- a/src/disk_interface_test.cc
+++ b/src/disk_interface_test.cc
@@ -76,6 +76,33 @@ TEST_F(DiskInterfaceTest, StatExistingFile) {
EXPECT_GT(disk_.Stat("file"), 1);
}
+#ifdef _WIN32
+TEST_F(DiskInterfaceTest, StatCache) {
+ disk_.AllowStatCache(true);
+
+ ASSERT_TRUE(Touch("file1"));
+ ASSERT_TRUE(Touch("fiLE2"));
+ ASSERT_TRUE(disk_.MakeDir("subdir"));
+ ASSERT_TRUE(Touch("subdir\\subfile1"));
+ ASSERT_TRUE(Touch("subdir\\SUBFILE2"));
+ ASSERT_TRUE(Touch("subdir\\SUBFILE3"));
+
+ EXPECT_GT(disk_.Stat("FIle1"), 1);
+ EXPECT_GT(disk_.Stat("file1"), 1);
+
+ EXPECT_GT(disk_.Stat("subdir/subfile2"), 1);
+ EXPECT_GT(disk_.Stat("sUbdir\\suBFile1"), 1);
+
+ // Test error cases.
+ disk_.quiet_ = true;
+ string bad_path("cc:\\foo");
+ EXPECT_EQ(-1, disk_.Stat(bad_path));
+ EXPECT_EQ(-1, disk_.Stat(bad_path));
+ EXPECT_EQ(0, disk_.Stat("nosuchfile"));
+ EXPECT_EQ(0, disk_.Stat("nosuchdir/nosuchfile"));
+}
+#endif
+
TEST_F(DiskInterfaceTest, ReadFile) {
string err;
EXPECT_EQ("", disk_.ReadFile("foobar", &err));
@@ -93,7 +120,18 @@ TEST_F(DiskInterfaceTest, ReadFile) {
}
TEST_F(DiskInterfaceTest, MakeDirs) {
- EXPECT_TRUE(disk_.MakeDirs("path/with/double//slash/"));
+ string path = "path/with/double//slash/";
+ EXPECT_TRUE(disk_.MakeDirs(path.c_str()));
+ FILE* f = fopen((path + "a_file").c_str(), "w");
+ EXPECT_TRUE(f);
+ EXPECT_EQ(0, fclose(f));
+#ifdef _WIN32
+ string path2 = "another\\with\\back\\\\slashes\\";
+ EXPECT_TRUE(disk_.MakeDirs(path2.c_str()));
+ FILE* f2 = fopen((path2 + "a_file").c_str(), "w");
+ EXPECT_TRUE(f2);
+ EXPECT_EQ(0, fclose(f2));
+#endif
}
TEST_F(DiskInterfaceTest, RemoveFile) {
@@ -109,7 +147,7 @@ struct StatTest : public StateTestWithBuiltinRules,
StatTest() : scan_(&state_, NULL, NULL, this) {}
// DiskInterface implementation.
- virtual TimeStamp Stat(const string& path);
+ virtual TimeStamp Stat(const string& path) const;
virtual bool WriteFile(const string& path, const string& contents) {
assert(false);
return true;
@@ -129,12 +167,12 @@ struct StatTest : public StateTestWithBuiltinRules,
DependencyScan scan_;
map<string, TimeStamp> mtimes_;
- vector<string> stats_;
+ mutable vector<string> stats_;
};
-TimeStamp StatTest::Stat(const string& path) {
+TimeStamp StatTest::Stat(const string& path) const {
stats_.push_back(path);
- map<string, TimeStamp>::iterator i = mtimes_.find(path);
+ map<string, TimeStamp>::const_iterator i = mtimes_.find(path);
if (i == mtimes_.end())
return 0; // File not found.
return i->second;
diff --git a/src/edit_distance.cc b/src/edit_distance.cc
index cc4483f..9553c6e 100644
--- a/src/edit_distance.cc
+++ b/src/edit_distance.cc
@@ -14,6 +14,7 @@
#include "edit_distance.h"
+#include <algorithm>
#include <vector>
int EditDistance(const StringPiece& s1,
diff --git a/src/graph.cc b/src/graph.cc
index 9801a7b..aa9c0e8 100644
--- a/src/graph.cc
+++ b/src/graph.cc
@@ -215,7 +215,10 @@ bool Edge::AllInputsReady() const {
/// An Env for an Edge, providing $in and $out.
struct EdgeEnv : public Env {
- explicit EdgeEnv(Edge* edge) : edge_(edge) {}
+ enum EscapeKind { kShellEscape, kDoNotEscape };
+
+ explicit EdgeEnv(Edge* edge, EscapeKind escape)
+ : edge_(edge), escape_in_out_(escape) {}
virtual string LookupVariable(const string& var);
/// Given a span of Nodes, construct a list of paths suitable for a command
@@ -225,6 +228,7 @@ struct EdgeEnv : public Env {
char sep);
Edge* edge_;
+ EscapeKind escape_in_out_;
};
string EdgeEnv::LookupVariable(const string& var) {
@@ -253,10 +257,12 @@ string EdgeEnv::MakePathList(vector<Node*>::iterator begin,
if (!result.empty())
result.push_back(sep);
const string& path = (*i)->path();
- if (path.find(" ") != string::npos) {
- result.append("\"");
- result.append(path);
- result.append("\"");
+ if (escape_in_out_ == kShellEscape) {
+#if _WIN32
+ GetWin32EscapedString(path, &result);
+#else
+ GetShellEscapedString(path, &result);
+#endif
} else {
result.append(path);
}
@@ -275,7 +281,7 @@ string Edge::EvaluateCommand(bool incl_rsp_file) {
}
string Edge::GetBinding(const string& key) {
- EdgeEnv env(this);
+ EdgeEnv env(this, EdgeEnv::kShellEscape);
return env.LookupVariable(key);
}
@@ -283,6 +289,16 @@ bool Edge::GetBindingBool(const string& key) {
return !GetBinding(key).empty();
}
+string Edge::GetUnescapedDepfile() {
+ EdgeEnv env(this, EdgeEnv::kDoNotEscape);
+ return env.LookupVariable("depfile");
+}
+
+string Edge::GetUnescapedRspfile() {
+ EdgeEnv env(this, EdgeEnv::kDoNotEscape);
+ return env.LookupVariable("rspfile");
+}
+
void Edge::Dump(const char* prefix) const {
printf("%s[ ", prefix);
for (vector<Node*>::const_iterator i = inputs_.begin();
@@ -308,6 +324,10 @@ bool Edge::is_phony() const {
return rule_ == &State::kPhonyRule;
}
+bool Edge::use_console() const {
+ return pool() == &State::kConsolePool;
+}
+
void Node::Dump(const char* prefix) const {
printf("%s <%s 0x%p> mtime: %d%s, (:%s), ",
prefix, path().c_str(), this,
@@ -330,7 +350,7 @@ bool ImplicitDepLoader::LoadDeps(Edge* edge, string* err) {
if (!deps_type.empty())
return LoadDepsFromLog(edge, err);
- string depfile = edge->GetBinding("depfile");
+ string depfile = edge->GetUnescapedDepfile();
if (!depfile.empty())
return LoadDepFile(edge, depfile, err);
diff --git a/src/graph.h b/src/graph.h
index 868413c..66e31b5 100644
--- a/src/graph.h
+++ b/src/graph.h
@@ -146,9 +146,15 @@ struct Edge {
/// full contents of a response file (if applicable)
string EvaluateCommand(bool incl_rsp_file = false);
+ /// Returns the shell-escaped value of |key|.
string GetBinding(const string& key);
bool GetBindingBool(const string& key);
+ /// Like GetBinding("depfile"), but without shell escaping.
+ string GetUnescapedDepfile();
+ /// Like GetBinding("rspfile"), but without shell escaping.
+ string GetUnescapedRspfile();
+
void Dump(const char* prefix="") const;
const Rule* rule_;
@@ -183,6 +189,7 @@ struct Edge {
}
bool is_phony() const;
+ bool use_console() const;
};
diff --git a/src/graph_test.cc b/src/graph_test.cc
index 8521216..14dc678 100644
--- a/src/graph_test.cc
+++ b/src/graph_test.cc
@@ -139,13 +139,18 @@ TEST_F(GraphTest, RootNodes) {
}
}
-TEST_F(GraphTest, VarInOutQuoteSpaces) {
+TEST_F(GraphTest, VarInOutPathEscaping) {
ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
-"build a$ b: cat nospace with$ space nospace2\n"));
+"build a$ b: cat no'space with$ space$$ no\"space2\n"));
Edge* edge = GetNode("a b")->in_edge();
- EXPECT_EQ("cat nospace \"with space\" nospace2 > \"a b\"",
+#if _WIN32
+ EXPECT_EQ("cat no'space \"with space$\" \"no\\\"space2\" > \"a b\"",
edge->EvaluateCommand());
+#else
+ EXPECT_EQ("cat 'no'\\''space' 'with space$' 'no\"space2' > 'a b'",
+ edge->EvaluateCommand());
+#endif
}
// Regression test for https://github.com/martine/ninja/issues/380
diff --git a/src/hash_map.h b/src/hash_map.h
index c63aa88..77e7586 100644
--- a/src/hash_map.h
+++ b/src/hash_map.h
@@ -15,6 +15,7 @@
#ifndef NINJA_MAP_H_
#define NINJA_MAP_H_
+#include <algorithm>
#include <string.h>
#include "string_piece.h"
diff --git a/src/includes_normalize_test.cc b/src/includes_normalize_test.cc
index 1713d5d..419996f 100644
--- a/src/includes_normalize_test.cc
+++ b/src/includes_normalize_test.cc
@@ -38,7 +38,7 @@ string GetCurDir() {
} // namespace
TEST(IncludesNormalize, WithRelative) {
- string currentdir = IncludesNormalize::ToLower(GetCurDir());
+ string currentdir = GetCurDir();
EXPECT_EQ("c", IncludesNormalize::Normalize("a/b/c", "a/b"));
EXPECT_EQ("a", IncludesNormalize::Normalize(IncludesNormalize::AbsPath("a"),
NULL));
diff --git a/src/line_printer.cc b/src/line_printer.cc
index 3537e88..ef1609c 100644
--- a/src/line_printer.cc
+++ b/src/line_printer.cc
@@ -26,7 +26,7 @@
#include "util.h"
-LinePrinter::LinePrinter() : have_blank_line_(true) {
+LinePrinter::LinePrinter() : have_blank_line_(true), console_locked_(false) {
#ifndef _WIN32
const char* term = getenv("TERM");
smart_terminal_ = isatty(1) && term && string(term) != "dumb";
@@ -43,6 +43,12 @@ LinePrinter::LinePrinter() : have_blank_line_(true) {
}
void LinePrinter::Print(string to_print, LineType type) {
+ if (console_locked_) {
+ line_buffer_ = to_print;
+ line_type_ = type;
+ return;
+ }
+
#ifdef _WIN32
CONSOLE_SCREEN_BUFFER_INFO csbi;
GetConsoleScreenBufferInfo(console_, &csbi);
@@ -101,13 +107,46 @@ void LinePrinter::Print(string to_print, LineType type) {
}
}
-void LinePrinter::PrintOnNewLine(const string& to_print) {
- if (!have_blank_line_)
- printf("\n");
- if (!to_print.empty()) {
+void LinePrinter::PrintOrBuffer(const char* data, size_t size) {
+ if (console_locked_) {
+ output_buffer_.append(data, size);
+ } else {
// Avoid printf and C strings, since the actual output might contain null
// bytes like UTF-16 does (yuck).
- fwrite(&to_print[0], sizeof(char), to_print.size(), stdout);
+ fwrite(data, 1, size, stdout);
+ }
+}
+
+void LinePrinter::PrintOnNewLine(const string& to_print) {
+ if (console_locked_ && !line_buffer_.empty()) {
+ output_buffer_.append(line_buffer_);
+ output_buffer_.append(1, '\n');
+ line_buffer_.clear();
+ }
+ if (!have_blank_line_) {
+ PrintOrBuffer("\n", 1);
+ }
+ if (!to_print.empty()) {
+ PrintOrBuffer(&to_print[0], to_print.size());
}
have_blank_line_ = to_print.empty() || *to_print.rbegin() == '\n';
}
+
+void LinePrinter::SetConsoleLocked(bool locked) {
+ if (locked == console_locked_)
+ return;
+
+ if (locked)
+ PrintOnNewLine("");
+
+ console_locked_ = locked;
+
+ if (!locked) {
+ PrintOnNewLine(output_buffer_);
+ if (!line_buffer_.empty()) {
+ Print(line_buffer_, line_type_);
+ }
+ output_buffer_.clear();
+ line_buffer_.clear();
+ }
+}
diff --git a/src/line_printer.h b/src/line_printer.h
index aea2817..55225e5 100644
--- a/src/line_printer.h
+++ b/src/line_printer.h
@@ -15,6 +15,7 @@
#ifndef NINJA_LINE_PRINTER_H_
#define NINJA_LINE_PRINTER_H_
+#include <stddef.h>
#include <string>
using namespace std;
@@ -37,6 +38,10 @@ struct LinePrinter {
/// Prints a string on a new line, not overprinting previous output.
void PrintOnNewLine(const string& to_print);
+ /// Lock or unlock the console. Any output sent to the LinePrinter while the
+ /// console is locked will not be printed until it is unlocked.
+ void SetConsoleLocked(bool locked);
+
private:
/// Whether we can do fancy terminal control codes.
bool smart_terminal_;
@@ -44,9 +49,24 @@ struct LinePrinter {
/// Whether the caret is at the beginning of a blank line.
bool have_blank_line_;
+ /// Whether console is locked.
+ bool console_locked_;
+
+ /// Buffered current line while console is locked.
+ string line_buffer_;
+
+ /// Buffered line type while console is locked.
+ LineType line_type_;
+
+ /// Buffered console output while console is locked.
+ string output_buffer_;
+
#ifdef _WIN32
void* console_;
#endif
+
+ /// Print the given data to the console, or buffer it if it is locked.
+ void PrintOrBuffer(const char *data, size_t size);
};
#endif // NINJA_LINE_PRINTER_H_
diff --git a/src/manifest_parser.cc b/src/manifest_parser.cc
index 20be7f3..6fa4f7c 100644
--- a/src/manifest_parser.cc
+++ b/src/manifest_parser.cc
@@ -296,16 +296,17 @@ bool ManifestParser::ParseEdge(string* err) {
if (!ExpectToken(Lexer::NEWLINE, err))
return false;
- // XXX scoped_ptr to handle error case.
- BindingEnv* env = new BindingEnv(env_);
-
- while (lexer_.PeekToken(Lexer::INDENT)) {
+ // Bindings on edges are rare, so allocate per-edge envs only when needed.
+ bool hasIdent = lexer_.PeekToken(Lexer::INDENT);
+ BindingEnv* env = hasIdent ? new BindingEnv(env_) : env_;
+ while (hasIdent) {
string key;
EvalString val;
if (!ParseLet(&key, &val, err))
return false;
env->AddBinding(key, val.Evaluate(env_));
+ hasIdent = lexer_.PeekToken(Lexer::INDENT);
}
Edge* edge = state_->AddEdge(rule);
diff --git a/src/manifest_parser_perftest.cc b/src/manifest_parser_perftest.cc
new file mode 100644
index 0000000..ca62fb2
--- /dev/null
+++ b/src/manifest_parser_perftest.cc
@@ -0,0 +1,118 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Tests manifest parser performance. Expects to be run in ninja's root
+// directory.
+
+#include <numeric>
+
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+
+#ifdef _WIN32
+#include "getopt.h"
+#include <direct.h>
+#else
+#include <getopt.h>
+#include <unistd.h>
+#endif
+
+#include "disk_interface.h"
+#include "graph.h"
+#include "manifest_parser.h"
+#include "metrics.h"
+#include "state.h"
+#include "util.h"
+
+struct RealFileReader : public ManifestParser::FileReader {
+ virtual bool ReadFile(const string& path, string* content, string* err) {
+ return ::ReadFile(path, content, err) == 0;
+ }
+};
+
+bool WriteFakeManifests(const string& dir) {
+ RealDiskInterface disk_interface;
+ if (disk_interface.Stat(dir + "/build.ninja") > 0)
+ return true;
+
+ printf("Creating manifest data..."); fflush(stdout);
+ int err = system(("python misc/write_fake_manifests.py " + dir).c_str());
+ printf("done.\n");
+ return err == 0;
+}
+
+int LoadManifests(bool measure_command_evaluation) {
+ string err;
+ RealFileReader file_reader;
+ State state;
+ ManifestParser parser(&state, &file_reader);
+ if (!parser.Load("build.ninja", &err)) {
+ fprintf(stderr, "Failed to read test data: %s\n", err.c_str());
+ exit(1);
+ }
+ // Doing an empty build involves reading the manifest and evaluating all
+ // commands required for the requested targets. So include command
+ // evaluation in the perftest by default.
+ int optimization_guard = 0;
+ if (measure_command_evaluation)
+ for (size_t i = 0; i < state.edges_.size(); ++i)
+ optimization_guard += state.edges_[i]->EvaluateCommand().size();
+ return optimization_guard;
+}
+
+int main(int argc, char* argv[]) {
+ bool measure_command_evaluation = true;
+ int opt;
+ while ((opt = getopt(argc, argv, const_cast<char*>("fh"))) != -1) {
+ switch (opt) {
+ case 'f':
+ measure_command_evaluation = false;
+ break;
+ case 'h':
+ default:
+ printf("usage: manifest_parser_perftest\n"
+"\n"
+"options:\n"
+" -f only measure manifest load time, not command evaluation time\n"
+ );
+ return 1;
+ }
+ }
+
+ const char kManifestDir[] = "build/manifest_perftest";
+
+ if (!WriteFakeManifests(kManifestDir)) {
+ fprintf(stderr, "Failed to write test data\n");
+ return 1;
+ }
+
+ if (chdir(kManifestDir) < 0)
+ Fatal("chdir: %s", strerror(errno));
+
+ const int kNumRepetitions = 5;
+ vector<int> times;
+ for (int i = 0; i < kNumRepetitions; ++i) {
+ int64_t start = GetTimeMillis();
+ int optimization_guard = LoadManifests(measure_command_evaluation);
+ int delta = (int)(GetTimeMillis() - start);
+ printf("%dms (hash: %x)\n", delta, optimization_guard);
+ times.push_back(delta);
+ }
+
+ int min = *min_element(times.begin(), times.end());
+ int max = *max_element(times.begin(), times.end());
+ float total = accumulate(times.begin(), times.end(), 0.0f);
+ printf("min %dms max %dms avg %.1fms\n", min, max, total / times.size());
+}
diff --git a/src/manifest_parser_test.cc b/src/manifest_parser_test.cc
index 5ed1584..152b965 100644
--- a/src/manifest_parser_test.cc
+++ b/src/manifest_parser_test.cc
@@ -236,7 +236,11 @@ TEST_F(ParserTest, Dollars) {
"build $x: foo y\n"
));
EXPECT_EQ("$dollar", state.bindings_.LookupVariable("x"));
+#ifdef _WIN32
EXPECT_EQ("$dollarbar$baz$blah", state.edges_[0]->EvaluateCommand());
+#else
+ EXPECT_EQ("'$dollar'bar$baz$blah", state.edges_[0]->EvaluateCommand());
+#endif
}
TEST_F(ParserTest, EscapeSpaces) {
@@ -762,6 +766,21 @@ TEST_F(ParserTest, MissingSubNinja) {
, err);
}
+TEST_F(ParserTest, DuplicateRuleInDifferentSubninjas) {
+ // Test that rules live in a global namespace and aren't scoped to subninjas.
+ files_["test.ninja"] = "rule cat\n"
+ " command = cat\n";
+ ManifestParser parser(&state, this);
+ string err;
+ EXPECT_FALSE(parser.ParseTest("rule cat\n"
+ " command = cat\n"
+ "subninja test.ninja\n", &err));
+ EXPECT_EQ("test.ninja:1: duplicate rule 'cat'\n"
+ "rule cat\n"
+ " ^ near here"
+ , err);
+}
+
TEST_F(ParserTest, Include) {
files_["include.ninja"] = "var = inner\n";
ASSERT_NO_FATAL_FAILURE(AssertParse(
diff --git a/src/metrics.cc b/src/metrics.cc
index ca4f97a..a7d3c7a 100644
--- a/src/metrics.cc
+++ b/src/metrics.cc
@@ -24,6 +24,8 @@
#include <windows.h>
#endif
+#include <algorithm>
+
#include "util.h"
Metrics* g_metrics = NULL;
diff --git a/src/msvc_helper-win32.cc b/src/msvc_helper-win32.cc
index 7c45029..e465279 100644
--- a/src/msvc_helper-win32.cc
+++ b/src/msvc_helper-win32.cc
@@ -48,14 +48,15 @@ string EscapeForDepfile(const string& path) {
}
// static
-string CLParser::FilterShowIncludes(const string& line) {
- static const char kMagicPrefix[] = "Note: including file: ";
+string CLParser::FilterShowIncludes(const string& line,
+ const string& deps_prefix) {
+ const string kDepsPrefixEnglish = "Note: including file: ";
const char* in = line.c_str();
const char* end = in + line.size();
-
- if (end - in > (int)sizeof(kMagicPrefix) - 1 &&
- memcmp(in, kMagicPrefix, sizeof(kMagicPrefix) - 1) == 0) {
- in += sizeof(kMagicPrefix) - 1;
+ const string& prefix = deps_prefix.empty() ? kDepsPrefixEnglish : deps_prefix;
+ if (end - in > (int)prefix.size() &&
+ memcmp(in, prefix.c_str(), (int)prefix.size()) == 0) {
+ in += prefix.size();
while (*in == ' ')
++in;
return line.substr(in - line.c_str());
@@ -81,7 +82,7 @@ bool CLParser::FilterInputFilename(string line) {
EndsWith(line, ".cpp");
}
-string CLParser::Parse(const string& output) {
+string CLParser::Parse(const string& output, const string& deps_prefix) {
string filtered_output;
// Loop over all lines in the output to process them.
@@ -92,7 +93,7 @@ string CLParser::Parse(const string& output) {
end = output.size();
string line = output.substr(start, end - start);
- string include = FilterShowIncludes(line);
+ string include = FilterShowIncludes(line, deps_prefix);
if (!include.empty()) {
include = IncludesNormalize::Normalize(include, NULL);
if (!IsSystemInclude(include))
@@ -140,7 +141,7 @@ int CLWrapper::Run(const string& command, string* output) {
STARTUPINFO startup_info = {};
startup_info.cb = sizeof(STARTUPINFO);
startup_info.hStdInput = nul;
- startup_info.hStdError = stdout_write;
+ startup_info.hStdError = ::GetStdHandle(STD_ERROR_HANDLE);
startup_info.hStdOutput = stdout_write;
startup_info.dwFlags |= STARTF_USESTDHANDLES;
diff --git a/src/msvc_helper.h b/src/msvc_helper.h
index e207485..5d7dcb0 100644
--- a/src/msvc_helper.h
+++ b/src/msvc_helper.h
@@ -27,7 +27,8 @@ struct CLParser {
/// Parse a line of cl.exe output and extract /showIncludes info.
/// If a dependency is extracted, returns a nonempty string.
/// Exposed for testing.
- static string FilterShowIncludes(const string& line);
+ static string FilterShowIncludes(const string& line,
+ const string& deps_prefix);
/// Return true if a mentioned include file is a system path.
/// Filtering these out reduces dependency information considerably.
@@ -41,7 +42,7 @@ struct CLParser {
/// Parse the full output of cl, returning the output (if any) that
/// should printed.
- string Parse(const string& output);
+ string Parse(const string& output, const string& deps_prefix);
set<string> includes_;
};
diff --git a/src/msvc_helper_main-win32.cc b/src/msvc_helper_main-win32.cc
index e3a7846..58bc797 100644
--- a/src/msvc_helper_main-win32.cc
+++ b/src/msvc_helper_main-win32.cc
@@ -31,6 +31,7 @@ void Usage() {
"options:\n"
" -e ENVFILE load environment block from ENVFILE as environment\n"
" -o FILE write output dependency information to FILE.d\n"
+" -p STRING localized prefix of msvc's /showIncludes output\n"
);
}
@@ -84,7 +85,8 @@ int MSVCHelperMain(int argc, char** argv) {
{ NULL, 0, NULL, 0 }
};
int opt;
- while ((opt = getopt_long(argc, argv, "e:o:h", kLongOptions, NULL)) != -1) {
+ string deps_prefix;
+ while ((opt = getopt_long(argc, argv, "e:o:p:h", kLongOptions, NULL)) != -1) {
switch (opt) {
case 'e':
envfile = optarg;
@@ -92,6 +94,9 @@ int MSVCHelperMain(int argc, char** argv) {
case 'o':
output_filename = optarg;
break;
+ case 'p':
+ deps_prefix = optarg;
+ break;
case 'h':
default:
Usage();
@@ -122,7 +127,7 @@ int MSVCHelperMain(int argc, char** argv) {
if (output_filename) {
CLParser parser;
- output = parser.Parse(output);
+ output = parser.Parse(output, deps_prefix);
WriteDepFileOrDie(output_filename, parser);
}
diff --git a/src/msvc_helper_test.cc b/src/msvc_helper_test.cc
index 02f2863..391c045 100644
--- a/src/msvc_helper_test.cc
+++ b/src/msvc_helper_test.cc
@@ -20,15 +20,19 @@
#include "util.h"
TEST(CLParserTest, ShowIncludes) {
- ASSERT_EQ("", CLParser::FilterShowIncludes(""));
+ ASSERT_EQ("", CLParser::FilterShowIncludes("", ""));
- ASSERT_EQ("", CLParser::FilterShowIncludes("Sample compiler output"));
+ ASSERT_EQ("", CLParser::FilterShowIncludes("Sample compiler output", ""));
ASSERT_EQ("c:\\Some Files\\foobar.h",
CLParser::FilterShowIncludes("Note: including file: "
- "c:\\Some Files\\foobar.h"));
+ "c:\\Some Files\\foobar.h", ""));
ASSERT_EQ("c:\\initspaces.h",
CLParser::FilterShowIncludes("Note: including file: "
- "c:\\initspaces.h"));
+ "c:\\initspaces.h", ""));
+ ASSERT_EQ("c:\\initspaces.h",
+ CLParser::FilterShowIncludes("Non-default prefix: inc file: "
+ "c:\\initspaces.h",
+ "Non-default prefix: inc file:"));
}
TEST(CLParserTest, FilterInputFilename) {
@@ -46,8 +50,9 @@ TEST(CLParserTest, ParseSimple) {
CLParser parser;
string output = parser.Parse(
"foo\r\n"
- "Note: including file: foo.h\r\n"
- "bar\r\n");
+ "Note: inc file prefix: foo.h\r\n"
+ "bar\r\n",
+ "Note: inc file prefix:");
ASSERT_EQ("foo\nbar\n", output);
ASSERT_EQ(1u, parser.includes_.size());
@@ -58,7 +63,8 @@ TEST(CLParserTest, ParseFilenameFilter) {
CLParser parser;
string output = parser.Parse(
"foo.cc\r\n"
- "cl: warning\r\n");
+ "cl: warning\r\n",
+ "");
ASSERT_EQ("cl: warning\n", output);
}
@@ -67,7 +73,8 @@ TEST(CLParserTest, ParseSystemInclude) {
string output = parser.Parse(
"Note: including file: c:\\Program Files\\foo.h\r\n"
"Note: including file: d:\\Microsoft Visual Studio\\bar.h\r\n"
- "Note: including file: path.h\r\n");
+ "Note: including file: path.h\r\n",
+ "");
// We should have dropped the first two includes because they look like
// system headers.
ASSERT_EQ("", output);
@@ -80,7 +87,8 @@ TEST(CLParserTest, DuplicatedHeader) {
string output = parser.Parse(
"Note: including file: foo.h\r\n"
"Note: including file: bar.h\r\n"
- "Note: including file: foo.h\r\n");
+ "Note: including file: foo.h\r\n",
+ "");
// We should have dropped one copy of foo.h.
ASSERT_EQ("", output);
ASSERT_EQ(2u, parser.includes_.size());
@@ -91,7 +99,8 @@ TEST(CLParserTest, DuplicatedHeaderPathConverted) {
string output = parser.Parse(
"Note: including file: sub/foo.h\r\n"
"Note: including file: bar.h\r\n"
- "Note: including file: sub\\foo.h\r\n");
+ "Note: including file: sub\\foo.h\r\n",
+ "");
// We should have dropped one copy of foo.h.
ASSERT_EQ("", output);
ASSERT_EQ(2u, parser.includes_.size());
@@ -110,3 +119,10 @@ TEST(MSVCHelperTest, EnvBlock) {
cl.Run("cmd /c \"echo foo is %foo%", &output);
ASSERT_EQ("foo is bar\r\n", output);
}
+
+TEST(MSVCHelperTest, NoReadOfStderr) {
+ CLWrapper cl;
+ string output;
+ cl.Run("cmd /c \"echo to stdout&& echo to stderr 1>&2", &output);
+ ASSERT_EQ("to stdout\r\n", output);
+}
diff --git a/src/ninja.cc b/src/ninja.cc
index a313ecb..a381e83 100644
--- a/src/ninja.cc
+++ b/src/ninja.cc
@@ -68,7 +68,7 @@ struct Options {
/// The Ninja main() loads up a series of data structures; various tools need
/// to poke into these, so store them as fields on an object.
-struct NinjaMain {
+struct NinjaMain : public BuildLogUser {
NinjaMain(const char* ninja_command, const BuildConfig& config) :
ninja_command_(ninja_command), config_(config) {}
@@ -137,6 +137,20 @@ struct NinjaMain {
/// Dump the output requested by '-d stats'.
void DumpMetrics();
+
+ virtual bool IsPathDead(StringPiece s) const {
+ Node* n = state_.LookupNode(s);
+ // Just checking n isn't enough: If an old output is both in the build log
+ // and in the deps log, it will have a Node object in state_. (It will also
+ // have an in edge if one of its inputs is another output that's in the deps
+ // log, but having a deps edge product an output thats input to another deps
+ // edge is rare, and the first recompaction will delete all old outputs from
+ // the deps log, and then a second recompaction will clear the build log,
+ // which seems good enough for this corner case.)
+ // Do keep entries around for files which still exist on disk, for
+ // generators that want to use this information.
+ return (!n || !n->in_edge()) && disk_interface_.Stat(s.AsString()) == 0;
+ }
};
/// Subtools, accessible via "-t foo".
@@ -444,9 +458,7 @@ int NinjaMain::ToolDeps(int argc, char** argv) {
if (argc == 0) {
for (vector<Node*>::const_iterator ni = deps_log_.nodes().begin();
ni != deps_log_.nodes().end(); ++ni) {
- // Only query for targets with an incoming edge and deps
- Edge* e = (*ni)->in_edge();
- if (e && !e->GetBinding("deps").empty())
+ if (deps_log_.IsDepsEntryLiveFor(*ni))
nodes.push_back(*ni);
}
} else {
@@ -621,6 +633,8 @@ int NinjaMain::ToolCompilationDatabase(int argc, char* argv[]) {
putchar('[');
for (vector<Edge*>::iterator e = state_.edges_.begin();
e != state_.edges_.end(); ++e) {
+ if ((*e)->inputs_.empty())
+ continue;
for (int i = 0; i != argc; ++i) {
if ((*e)->rule_->name() == argv[i]) {
if (!first)
@@ -748,6 +762,9 @@ bool DebugEnable(const string& name) {
" stats print operation counts/timing info\n"
" explain explain what caused a command to execute\n"
" keeprsp don't delete @response files on success\n"
+#ifdef _WIN32
+" nostatcache don't batch stat() calls per directory and cache them\n"
+#endif
"multiple modes can be enabled via -d FOO -d BAR\n");
return false;
} else if (name == "stats") {
@@ -759,9 +776,13 @@ bool DebugEnable(const string& name) {
} else if (name == "keeprsp") {
g_keep_rsp = true;
return true;
+ } else if (name == "nostatcache") {
+ g_experimental_statcache = false;
+ return true;
} else {
const char* suggestion =
- SpellcheckString(name.c_str(), "stats", "explain", NULL);
+ SpellcheckString(name.c_str(), "stats", "explain", "keeprsp",
+ "nostatcache", NULL);
if (suggestion) {
Error("unknown debug setting '%s', did you mean '%s'?",
name.c_str(), suggestion);
@@ -789,14 +810,14 @@ bool NinjaMain::OpenBuildLog(bool recompact_only) {
}
if (recompact_only) {
- bool success = build_log_.Recompact(log_path, &err);
+ bool success = build_log_.Recompact(log_path, *this, &err);
if (!success)
Error("failed recompaction: %s", err.c_str());
return success;
}
if (!config_.dry_run) {
- if (!build_log_.OpenForWrite(log_path, &err)) {
+ if (!build_log_.OpenForWrite(log_path, *this, &err)) {
Error("opening build log: %s", err.c_str());
return false;
}
@@ -870,6 +891,8 @@ int NinjaMain::RunBuild(int argc, char** argv) {
return 1;
}
+ disk_interface_.AllowStatCache(g_experimental_statcache);
+
Builder builder(&state_, config_, &build_log_, &deps_log_, &disk_interface_);
for (size_t i = 0; i < targets.size(); ++i) {
if (!builder.AddTarget(targets[i], &err)) {
@@ -883,6 +906,9 @@ int NinjaMain::RunBuild(int argc, char** argv) {
}
}
+ // Make sure restat rules do not see stale timestamps.
+ disk_interface_.AllowStatCache(false);
+
if (builder.AlreadyUpToDate()) {
printf("ninja: no work to do.\n");
return 0;
diff --git a/src/state.cc b/src/state.cc
index 9b6160b..7258272 100644
--- a/src/state.cc
+++ b/src/state.cc
@@ -69,11 +69,13 @@ bool Pool::WeightedEdgeCmp(const Edge* a, const Edge* b) {
}
Pool State::kDefaultPool("", 0);
+Pool State::kConsolePool("console", 1);
const Rule State::kPhonyRule("phony");
State::State() {
AddRule(&kPhonyRule);
AddPool(&kDefaultPool);
+ AddPool(&kConsolePool);
}
void State::AddRule(const Rule* rule) {
@@ -118,9 +120,9 @@ Node* State::GetNode(StringPiece path) {
return node;
}
-Node* State::LookupNode(StringPiece path) {
+Node* State::LookupNode(StringPiece path) const {
METRIC_RECORD("lookup node");
- Paths::iterator i = paths_.find(path);
+ Paths::const_iterator i = paths_.find(path);
if (i != paths_.end())
return i->second;
return NULL;
diff --git a/src/state.h b/src/state.h
index bde75ff..c382dc0 100644
--- a/src/state.h
+++ b/src/state.h
@@ -82,6 +82,7 @@ struct Pool {
/// Global state (file status, loaded rules) for a single run.
struct State {
static Pool kDefaultPool;
+ static Pool kConsolePool;
static const Rule kPhonyRule;
State();
@@ -95,7 +96,7 @@ struct State {
Edge* AddEdge(const Rule* rule);
Node* GetNode(StringPiece path);
- Node* LookupNode(StringPiece path);
+ Node* LookupNode(StringPiece path) const;
Node* SpellcheckNode(const string& path);
void AddIn(Edge* edge, StringPiece path);
diff --git a/src/subprocess-posix.cc b/src/subprocess-posix.cc
index a9af756..743e406 100644
--- a/src/subprocess-posix.cc
+++ b/src/subprocess-posix.cc
@@ -25,7 +25,8 @@
#include "util.h"
-Subprocess::Subprocess() : fd_(-1), pid_(-1) {
+Subprocess::Subprocess(bool use_console) : fd_(-1), pid_(-1),
+ use_console_(use_console) {
}
Subprocess::~Subprocess() {
if (fd_ >= 0)
@@ -58,29 +59,34 @@ bool Subprocess::Start(SubprocessSet* set, const string& command) {
// Track which fd we use to report errors on.
int error_pipe = output_pipe[1];
do {
- if (setpgid(0, 0) < 0)
- break;
-
if (sigaction(SIGINT, &set->old_act_, 0) < 0)
break;
if (sigprocmask(SIG_SETMASK, &set->old_mask_, 0) < 0)
break;
- // Open /dev/null over stdin.
- int devnull = open("/dev/null", O_RDONLY);
- if (devnull < 0)
- break;
- if (dup2(devnull, 0) < 0)
- break;
- close(devnull);
-
- if (dup2(output_pipe[1], 1) < 0 ||
- dup2(output_pipe[1], 2) < 0)
- break;
-
- // Now can use stderr for errors.
- error_pipe = 2;
- close(output_pipe[1]);
+ if (!use_console_) {
+ // Put the child in its own process group, so ctrl-c won't reach it.
+ if (setpgid(0, 0) < 0)
+ break;
+
+ // Open /dev/null over stdin.
+ int devnull = open("/dev/null", O_RDONLY);
+ if (devnull < 0)
+ break;
+ if (dup2(devnull, 0) < 0)
+ break;
+ close(devnull);
+
+ if (dup2(output_pipe[1], 1) < 0 ||
+ dup2(output_pipe[1], 2) < 0)
+ break;
+
+ // Now can use stderr for errors.
+ error_pipe = 2;
+ close(output_pipe[1]);
+ }
+ // In the console case, output_pipe is still inherited by the child and
+ // closed when the subprocess finishes, which then notifies ninja.
execl("/bin/sh", "/bin/sh", "-c", command.c_str(), (char *) NULL);
} while (false);
@@ -168,8 +174,8 @@ SubprocessSet::~SubprocessSet() {
Fatal("sigprocmask: %s", strerror(errno));
}
-Subprocess *SubprocessSet::Add(const string& command) {
- Subprocess *subprocess = new Subprocess;
+Subprocess *SubprocessSet::Add(const string& command, bool use_console) {
+ Subprocess *subprocess = new Subprocess(use_console);
if (!subprocess->Start(this, command)) {
delete subprocess;
return 0;
@@ -279,7 +285,10 @@ Subprocess* SubprocessSet::NextFinished() {
void SubprocessSet::Clear() {
for (vector<Subprocess*>::iterator i = running_.begin();
i != running_.end(); ++i)
- kill(-(*i)->pid_, SIGINT);
+ // Since the foreground process is in our process group, it will receive a
+ // SIGINT at the same time as us.
+ if (!(*i)->use_console_)
+ kill(-(*i)->pid_, SIGINT);
for (vector<Subprocess*>::iterator i = running_.begin();
i != running_.end(); ++i)
delete *i;
diff --git a/src/subprocess-win32.cc b/src/subprocess-win32.cc
index 1b230b6..fad66e8 100644
--- a/src/subprocess-win32.cc
+++ b/src/subprocess-win32.cc
@@ -14,13 +14,16 @@
#include "subprocess.h"
+#include <assert.h>
#include <stdio.h>
#include <algorithm>
#include "util.h"
-Subprocess::Subprocess() : child_(NULL) , overlapped_(), is_reading_(false) {
+Subprocess::Subprocess(bool use_console) : child_(NULL) , overlapped_(),
+ is_reading_(false),
+ use_console_(use_console) {
}
Subprocess::~Subprocess() {
@@ -86,18 +89,25 @@ bool Subprocess::Start(SubprocessSet* set, const string& command) {
STARTUPINFOA startup_info;
memset(&startup_info, 0, sizeof(startup_info));
startup_info.cb = sizeof(STARTUPINFO);
- startup_info.dwFlags = STARTF_USESTDHANDLES;
- startup_info.hStdInput = nul;
- startup_info.hStdOutput = child_pipe;
- startup_info.hStdError = child_pipe;
+ if (!use_console_) {
+ startup_info.dwFlags = STARTF_USESTDHANDLES;
+ startup_info.hStdInput = nul;
+ startup_info.hStdOutput = child_pipe;
+ startup_info.hStdError = child_pipe;
+ }
+ // In the console case, child_pipe is still inherited by the child and closed
+ // when the subprocess finishes, which then notifies ninja.
PROCESS_INFORMATION process_info;
memset(&process_info, 0, sizeof(process_info));
+ // Ninja handles ctrl-c, except for subprocesses in console pools.
+ DWORD process_flags = use_console_ ? 0 : CREATE_NEW_PROCESS_GROUP;
+
// Do not prepend 'cmd /c' on Windows, this breaks command
// lines greater than 8,191 chars.
if (!CreateProcessA(NULL, (char*)command.c_str(), NULL, NULL,
- /* inherit handles */ TRUE, CREATE_NEW_PROCESS_GROUP,
+ /* inherit handles */ TRUE, process_flags,
NULL, NULL,
&startup_info, &process_info)) {
DWORD error = GetLastError();
@@ -213,8 +223,8 @@ BOOL WINAPI SubprocessSet::NotifyInterrupted(DWORD dwCtrlType) {
return FALSE;
}
-Subprocess *SubprocessSet::Add(const string& command) {
- Subprocess *subprocess = new Subprocess;
+Subprocess *SubprocessSet::Add(const string& command, bool use_console) {
+ Subprocess *subprocess = new Subprocess(use_console);
if (!subprocess->Start(this, command)) {
delete subprocess;
return 0;
@@ -266,7 +276,9 @@ Subprocess* SubprocessSet::NextFinished() {
void SubprocessSet::Clear() {
for (vector<Subprocess*>::iterator i = running_.begin();
i != running_.end(); ++i) {
- if ((*i)->child_) {
+ // Since the foreground process is in our process group, it will receive a
+ // CTRL_C_EVENT or CTRL_BREAK_EVENT at the same time as us.
+ if ((*i)->child_ && !(*i)->use_console_) {
if (!GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT,
GetProcessId((*i)->child_))) {
Win32Fatal("GenerateConsoleCtrlEvent");
diff --git a/src/subprocess.h b/src/subprocess.h
index 4c1629c..b7a1a4c 100644
--- a/src/subprocess.h
+++ b/src/subprocess.h
@@ -44,7 +44,7 @@ struct Subprocess {
const string& GetOutput() const;
private:
- Subprocess();
+ Subprocess(bool use_console);
bool Start(struct SubprocessSet* set, const string& command);
void OnPipeReady();
@@ -64,6 +64,7 @@ struct Subprocess {
int fd_;
pid_t pid_;
#endif
+ bool use_console_;
friend struct SubprocessSet;
};
@@ -75,7 +76,7 @@ struct SubprocessSet {
SubprocessSet();
~SubprocessSet();
- Subprocess* Add(const string& command);
+ Subprocess* Add(const string& command, bool use_console = false);
bool DoWork();
Subprocess* NextFinished();
void Clear();
diff --git a/src/subprocess_test.cc b/src/subprocess_test.cc
index 9f8dcea..775a13a 100644
--- a/src/subprocess_test.cc
+++ b/src/subprocess_test.cc
@@ -95,6 +95,21 @@ TEST_F(SubprocessTest, InterruptParent) {
ADD_FAILURE() << "We should have been interrupted";
}
+TEST_F(SubprocessTest, Console) {
+ // Skip test if we don't have the console ourselves.
+ if (isatty(0) && isatty(1) && isatty(2)) {
+ Subprocess* subproc = subprocs_.Add("test -t 0 -a -t 1 -a -t 2",
+ /*use_console=*/true);
+ ASSERT_NE((Subprocess *) 0, subproc);
+
+ while (!subproc->Done()) {
+ subprocs_.DoWork();
+ }
+
+ EXPECT_EQ(ExitSuccess, subproc->Finish());
+ }
+}
+
#endif
TEST_F(SubprocessTest, SetWithSingle) {
diff --git a/src/test.cc b/src/test.cc
index 45a9226..21015ed 100644
--- a/src/test.cc
+++ b/src/test.cc
@@ -105,8 +105,8 @@ void VirtualFileSystem::Create(const string& path,
files_created_.insert(path);
}
-TimeStamp VirtualFileSystem::Stat(const string& path) {
- FileMap::iterator i = files_.find(path);
+TimeStamp VirtualFileSystem::Stat(const string& path) const {
+ FileMap::const_iterator i = files_.find(path);
if (i != files_.end())
return i->second.mtime;
return 0;
diff --git a/src/test.h b/src/test.h
index 9f29e07..f34b877 100644
--- a/src/test.h
+++ b/src/test.h
@@ -59,7 +59,7 @@ struct VirtualFileSystem : public DiskInterface {
}
// DiskInterface
- virtual TimeStamp Stat(const string& path);
+ virtual TimeStamp Stat(const string& path) const;
virtual bool WriteFile(const string& path, const string& contents);
virtual bool MakeDir(const string& path);
virtual string ReadFile(const string& path, string* err);
diff --git a/src/util.cc b/src/util.cc
index 6ba3c6c..484b0c1 100644
--- a/src/util.cc
+++ b/src/util.cc
@@ -20,6 +20,7 @@
#include <share.h>
#endif
+#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
@@ -175,6 +176,109 @@ bool CanonicalizePath(char* path, size_t* len, string* err) {
return true;
}
+static inline bool IsKnownShellSafeCharacter(char ch) {
+ if ('A' <= ch && ch <= 'Z') return true;
+ if ('a' <= ch && ch <= 'z') return true;
+ if ('0' <= ch && ch <= '9') return true;
+
+ switch (ch) {
+ case '_':
+ case '+':
+ case '-':
+ case '.':
+ case '/':
+ return true;
+ default:
+ return false;
+ }
+}
+
+static inline bool IsKnownWin32SafeCharacter(char ch) {
+ switch (ch) {
+ case ' ':
+ case '"':
+ return false;
+ default:
+ return true;
+ }
+}
+
+static inline bool StringNeedsShellEscaping(const string& input) {
+ for (size_t i = 0; i < input.size(); ++i) {
+ if (!IsKnownShellSafeCharacter(input[i])) return true;
+ }
+ return false;
+}
+
+static inline bool StringNeedsWin32Escaping(const string& input) {
+ for (size_t i = 0; i < input.size(); ++i) {
+ if (!IsKnownWin32SafeCharacter(input[i])) return true;
+ }
+ return false;
+}
+
+void GetShellEscapedString(const string& input, string* result) {
+ assert(result);
+
+ if (!StringNeedsShellEscaping(input)) {
+ result->append(input);
+ return;
+ }
+
+ const char kQuote = '\'';
+ const char kEscapeSequence[] = "'\\'";
+
+ result->push_back(kQuote);
+
+ string::const_iterator span_begin = input.begin();
+ for (string::const_iterator it = input.begin(), end = input.end(); it != end;
+ ++it) {
+ if (*it == kQuote) {
+ result->append(span_begin, it);
+ result->append(kEscapeSequence);
+ span_begin = it;
+ }
+ }
+ result->append(span_begin, input.end());
+ result->push_back(kQuote);
+}
+
+
+void GetWin32EscapedString(const string& input, string* result) {
+ assert(result);
+ if (!StringNeedsWin32Escaping(input)) {
+ result->append(input);
+ return;
+ }
+
+ const char kQuote = '"';
+ const char kBackslash = '\\';
+
+ result->push_back(kQuote);
+ size_t consecutive_backslash_count = 0;
+ string::const_iterator span_begin = input.begin();
+ for (string::const_iterator it = input.begin(), end = input.end(); it != end;
+ ++it) {
+ switch (*it) {
+ case kBackslash:
+ ++consecutive_backslash_count;
+ break;
+ case kQuote:
+ result->append(span_begin, it);
+ result->append(consecutive_backslash_count + 1, kBackslash);
+ span_begin = it;
+ consecutive_backslash_count = 0;
+ break;
+ default:
+ consecutive_backslash_count = 0;
+ break;
+ }
+ }
+ result->append(span_begin, input.end());
+ result->append(consecutive_backslash_count, kBackslash);
+ result->push_back(kQuote);
+}
+
int ReadFile(const string& path, string* contents, string* err) {
FILE* f = fopen(path.c_str(), "r");
if (!f) {
diff --git a/src/util.h b/src/util.h
index 6788410..7101770 100644
--- a/src/util.h
+++ b/src/util.h
@@ -45,6 +45,13 @@ bool CanonicalizePath(string* path, string* err);
bool CanonicalizePath(char* path, size_t* len, string* err);
+/// Appends |input| to |*result|, escaping according to the whims of either
+/// Bash, or Win32's CommandLineToArgvW().
+/// Appends the string directly to |result| without modification if we can
+/// determine that it contains no problematic characters.
+void GetShellEscapedString(const string& input, string* result);
+void GetWin32EscapedString(const string& input, string* result);
+
/// Read a file to a string (in text mode: with CRLF conversion
/// on Windows).
/// Returns -errno and fills in \a err on error.
diff --git a/src/util_test.cc b/src/util_test.cc
index 1e29053..b58d15e 100644
--- a/src/util_test.cc
+++ b/src/util_test.cc
@@ -136,6 +136,37 @@ TEST(CanonicalizePath, NotNullTerminated) {
EXPECT_EQ("file ./file bar/.", string(path));
}
+TEST(PathEscaping, TortureTest) {
+ string result;
+
+ GetWin32EscapedString("foo bar\\\"'$@d!st!c'\\path'\\", &result);
+ EXPECT_EQ("\"foo bar\\\\\\\"'$@d!st!c'\\path'\\\\\"", result);
+ result.clear();
+
+ GetShellEscapedString("foo bar\"/'$@d!st!c'/path'", &result);
+ EXPECT_EQ("'foo bar\"/'\\''$@d!st!c'\\''/path'\\'''", result);
+}
+
+TEST(PathEscaping, SensiblePathsAreNotNeedlesslyEscaped) {
+ const char* path = "some/sensible/path/without/crazy/characters.c++";
+ string result;
+
+ GetWin32EscapedString(path, &result);
+ EXPECT_EQ(path, result);
+ result.clear();
+
+ GetShellEscapedString(path, &result);
+ EXPECT_EQ(path, result);
+}
+
+TEST(PathEscaping, SensibleWin32PathsAreNotNeedlesslyEscaped) {
+ const char* path = "some\\sensible\\path\\without\\crazy\\characters.c++";
+ string result;
+
+ GetWin32EscapedString(path, &result);
+ EXPECT_EQ(path, result);
+}
+
TEST(StripAnsiEscapeCodes, EscapeAtEnd) {
string stripped = StripAnsiEscapeCodes("foo\33");
EXPECT_EQ("foo", stripped);
diff --git a/src/version.cc b/src/version.cc
index 17c71aa..6d2d37c 100644
--- a/src/version.cc
+++ b/src/version.cc
@@ -18,7 +18,7 @@
#include "util.h"
-const char* kNinjaVersion = "1.4.0";
+const char* kNinjaVersion = "1.5.0";
void ParseVersion(const string& version, int* major, int* minor) {
size_t end = version.find('.');