diff options
Diffstat (limited to 'ninja')
31 files changed, 758 insertions, 129 deletions
diff --git a/ninja/.clang-format b/ninja/.clang-format new file mode 100644 index 00000000000..1841c036f9c --- /dev/null +++ b/ninja/.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/ninja/configure.py b/ninja/configure.py index da2f6ef478d..c5a6abdd93c 100755 --- a/ninja/configure.py +++ b/ninja/configure.py @@ -377,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/ninja/doc/manual.asciidoc b/ninja/doc/manual.asciidoc index 67fcbfd2788..18760dde3f4 100644 --- a/ninja/doc/manual.asciidoc +++ b/ninja/doc/manual.asciidoc @@ -648,6 +648,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 -------------------- diff --git a/ninja/misc/bash-completion b/ninja/misc/bash-completion index 2d6975b9450..6edf4dfea02 100644 --- a/ninja/misc/bash-completion +++ b/ninja/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/ninja/misc/ninja_syntax.py b/ninja/misc/ninja_syntax.py index d69e3e49e2d..14b932ff4c8 100644 --- a/ninja/misc/ninja_syntax.py +++ b/ninja/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/ninja/misc/write_fake_manifests.py b/ninja/misc/write_fake_manifests.py new file mode 100644 index 00000000000..837007e603b --- /dev/null +++ b/ninja/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/ninja/misc/zsh-completion b/ninja/misc/zsh-completion index cd0edfbd97a..2fe16fb0499 100644 --- a/ninja/misc/zsh-completion +++ b/ninja/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/ninja/src/build.cc b/ninja/src/build.cc index f91ff2fb2b3..91f1754c150 100644 --- a/ninja/src/build.cc +++ b/ninja/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)); @@ -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; } diff --git a/ninja/src/build_test.cc b/ninja/src/build_test.cc index 86a911bd281..c414c88fa86 100644 --- a/ninja/src/build_test.cc +++ b/ninja/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" @@ -515,7 +530,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"); @@ -539,6 +555,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 @@ -1911,3 +1936,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/ninja/src/clean_test.cc b/ninja/src/clean_test.cc index 04cff734aa7..4a00fd8fe37 100644 --- a/ninja/src/clean_test.cc +++ b/ninja/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) { diff --git a/ninja/src/depfile_parser.cc b/ninja/src/depfile_parser.cc index 49c7d7b316c..4ca3943ff35 100644 --- a/ninja/src/depfile_parser.cc +++ b/ninja/src/depfile_parser.cc @@ -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, @@ -114,27 +114,29 @@ bool DepfileParser::Parse(string* content, string* err) { if (yych != '\\') goto yy9; } } else { - if (yych <= 'z') { + if (yych <= '{') { if (yych == '`') goto yy9; goto yy5; } else { - if (yych == '~') goto yy5; + if (yych <= '|') goto yy9; + if (yych <= '~') goto yy5; goto yy9; } } } ++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 { @@ -224,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/ninja/src/depfile_parser.in.cc b/ninja/src/depfile_parser.in.cc index 8bb6d8444d7..b59baf05c93 100644 --- a/ninja/src/depfile_parser.in.cc +++ b/ninja/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/ninja/src/parser_perftest.cc b/ninja/src/depfile_parser_perftest.cc index b21522168df..b21522168df 100644 --- a/ninja/src/parser_perftest.cc +++ b/ninja/src/depfile_parser_perftest.cc diff --git a/ninja/src/depfile_parser_test.cc b/ninja/src/depfile_parser_test.cc index 0f6771a9901..a5f33214099 100644 --- a/ninja/src/depfile_parser_test.cc +++ b/ninja/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/ninja/src/disk_interface.cc b/ninja/src/disk_interface.cc index 3233144c893..4dfae1a3f9c 100644 --- a/ninja/src/disk_interface.cc +++ b/ninja/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); } @@ -146,6 +149,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; } diff --git a/ninja/src/disk_interface_test.cc b/ninja/src/disk_interface_test.cc index 55822a68ea0..51a1d14d894 100644 --- a/ninja/src/disk_interface_test.cc +++ b/ninja/src/disk_interface_test.cc @@ -93,7 +93,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) { diff --git a/ninja/src/graph.cc b/ninja/src/graph.cc index 65f92442170..71213421449 100644 --- a/ninja/src/graph.cc +++ b/ninja/src/graph.cc @@ -305,6 +305,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, diff --git a/ninja/src/graph.h b/ninja/src/graph.h index 868413c3bce..6cd7f25452c 100644 --- a/ninja/src/graph.h +++ b/ninja/src/graph.h @@ -183,6 +183,7 @@ struct Edge { } bool is_phony() const; + bool use_console() const; }; diff --git a/ninja/src/line_printer.cc b/ninja/src/line_printer.cc index 3537e886e69..ef1609c28f5 100644 --- a/ninja/src/line_printer.cc +++ b/ninja/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/ninja/src/line_printer.h b/ninja/src/line_printer.h index aea28172729..55225e52117 100644 --- a/ninja/src/line_printer.h +++ b/ninja/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/ninja/src/manifest_parser.cc b/ninja/src/manifest_parser.cc index 20be7f3777f..6fa4f7c8b44 100644 --- a/ninja/src/manifest_parser.cc +++ b/ninja/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/ninja/src/manifest_parser_perftest.cc b/ninja/src/manifest_parser_perftest.cc new file mode 100644 index 00000000000..e40468fbdb0 --- /dev/null +++ b/ninja/src/manifest_parser_perftest.cc @@ -0,0 +1,114 @@ +// 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 <stdio.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; + } + + chdir(kManifestDir); + + 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/ninja/src/msvc_helper-win32.cc b/ninja/src/msvc_helper-win32.cc index d2e2eb536c4..e4652794a7b 100644 --- a/ninja/src/msvc_helper-win32.cc +++ b/ninja/src/msvc_helper-win32.cc @@ -141,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/ninja/src/msvc_helper_test.cc b/ninja/src/msvc_helper_test.cc index 48fbe21dfee..391c04568c3 100644 --- a/ninja/src/msvc_helper_test.cc +++ b/ninja/src/msvc_helper_test.cc @@ -119,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/ninja/src/ninja.cc b/ninja/src/ninja.cc index 03ca83b544c..50de43edbab 100644 --- a/ninja/src/ninja.cc +++ b/ninja/src/ninja.cc @@ -631,6 +631,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) diff --git a/ninja/src/state.cc b/ninja/src/state.cc index 33f8423312a..7258272056c 100644 --- a/ninja/src/state.cc +++ b/ninja/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) { diff --git a/ninja/src/state.h b/ninja/src/state.h index bcb0eff75ff..c382dc014cc 100644 --- a/ninja/src/state.h +++ b/ninja/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(); diff --git a/ninja/src/subprocess-posix.cc b/ninja/src/subprocess-posix.cc index a9af756dca4..743e4066136 100644 --- a/ninja/src/subprocess-posix.cc +++ b/ninja/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/ninja/src/subprocess-win32.cc b/ninja/src/subprocess-win32.cc index 1b230b640b1..fad66e895c0 100644 --- a/ninja/src/subprocess-win32.cc +++ b/ninja/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/ninja/src/subprocess.h b/ninja/src/subprocess.h index 4c1629c89b3..b7a1a4c048e 100644 --- a/ninja/src/subprocess.h +++ b/ninja/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/ninja/src/subprocess_test.cc b/ninja/src/subprocess_test.cc index 9f8dcea68cb..775a13a6c93 100644 --- a/ninja/src/subprocess_test.cc +++ b/ninja/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) { |