Add the "Phony Output" concept
These are like Make's .PHONY rules in that they can have a command, but
are always considered dirty, and will re-run on every build.
Adds `-w usesphonyoutputs=yes` to control whether the output directory
check happens at all, as before this is used, Make .PHONY rules can be
real rules that can trigger the output directory check. This will also
be useful to control the next warnings about outputs existing and being
too old.
Also replicate the checks from Kati where non-phony outputs nodes cannot
depend on phony output nodes (otherwise they'd always rebuild).
Test: ninja_tests (run by build-prebuilts.sh)
Change-Id: Iea561f775434e1b062aedfbf7014bcdbaa66a5db
diff --git a/doc/manual.asciidoc b/doc/manual.asciidoc
index e298feb..c2b00db 100644
--- a/doc/manual.asciidoc
+++ b/doc/manual.asciidoc
@@ -816,6 +816,31 @@
the full command or its description; if a command fails, the full command
line will always be printed before the command's output.
+`phony_output`:: _(Android-specific patch)_ if present, Ninja will not attempt
+ to look for them on disk. These rules are considered always dirty, and will
+ run every time they're depended upon. This behavior is very similar to Make's
+ `.PHONY` concept.
++
+This can be similar to the `phony` rule, but can have an attached `command`.
+`phony` rules are also only considered dirty in two cases: if their inputs are
+dirty, or if they have no inputs and a file with the same name does not exist on
+disk.
++
+Other build rules may not depend on `phony_output` rules unless they are also
+`phony_output`, so that it's not possible to accidentally cause everything to
+rebuild on every run.
++
+When `-w usesphonyoutputs=yes` is set on the ninja command line, it becomes an
+error for a `phony` rule to cause rebuilds, so that users can be found and
+migrated.
++
+Properly using `phony_output` and turning on `-w usesphonyoutputs=yes` allows
+the `-w outputdir={err,warn}` (consider output files that are directories as
+errors/warnings), `-w missingoutfile={err,warn}` (error/warn when an output file
+does not exist after a successful rule execution), and `-w oldoutput={err,warn}`
+(error/warn when an output file is not updated after a successful non-restat
+rule execution) flags to function.
+
`generator`:: if present, specifies that this rule is used to
re-invoke the generator program. Files built using `generator`
rules are treated specially in two ways: firstly, they will not be
diff --git a/src/build.cc b/src/build.cc
index 0ce39a1..fde9c78 100644
--- a/src/build.cc
+++ b/src/build.cc
@@ -230,6 +230,10 @@
if ((*oe)->deps_missing_)
continue;
+ // No need to clean a phony output edge, as it's always dirty
+ if ((*oe)->IsPhonyOutput())
+ continue;
+
// If all non-order-only inputs for this edge are now clean,
// we might have changed the dirty state of the outputs.
vector<Node*>::iterator
@@ -355,7 +359,7 @@
int64_t start_time_millis)
: state_(state), config_(config), status_(status),
start_time_millis_(start_time_millis), disk_interface_(disk_interface),
- scan_(state, build_log, deps_log, disk_interface) {
+ scan_(state, build_log, deps_log, disk_interface, config.uses_phony_outputs) {
}
Builder::~Builder() {
@@ -369,6 +373,8 @@
for (vector<Edge*>::iterator e = active_edges.begin();
e != active_edges.end(); ++e) {
+ if ((*e)->IsPhonyOutput())
+ continue;
string depfile = (*e)->GetUnescapedDepfile();
for (vector<Node*>::iterator o = (*e)->outputs_.begin();
o != (*e)->outputs_.end(); ++o) {
@@ -528,12 +534,14 @@
status_->BuildEdgeStarted(edge, start_time_millis);
- // Create directories necessary for outputs.
- // XXX: this will block; do we care?
- for (vector<Node*>::iterator o = edge->outputs_.begin();
- o != edge->outputs_.end(); ++o) {
- if (!disk_interface_->MakeDirs((*o)->path()))
- return false;
+ if (!edge->IsPhonyOutput()) {
+ // Create directories necessary for outputs.
+ // XXX: this will block; do we care?
+ for (vector<Node*>::iterator o = edge->outputs_.begin();
+ o != edge->outputs_.end(); ++o) {
+ if (!disk_interface_->MakeDirs((*o)->path()))
+ return false;
+ }
}
// Create response file, if needed
@@ -558,24 +566,27 @@
METRIC_RECORD("FinishCommand");
Edge* edge = result->edge;
+ bool phony_output = edge->IsPhonyOutput();
- // First try to extract dependencies from the result, if any.
- // This must happen first as it filters the command output (we want
- // to filter /showIncludes output, even on compile failure) and
- // extraction itself can fail, which makes the command fail from a
- // 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_prefix, &deps_nodes,
- &extract_err) &&
- result->success()) {
- if (!result->output.empty())
- result->output.append("\n");
- result->output.append(extract_err);
- result->status = ExitFailure;
+ if (!phony_output) {
+ // First try to extract dependencies from the result, if any.
+ // This must happen first as it filters the command output (we want
+ // to filter /showIncludes output, even on compile failure) and
+ // extraction itself can fail, which makes the command fail from a
+ // build perspective.
+ const string deps_prefix = edge->GetBinding("msvc_deps_prefix");
+ if (!deps_type.empty()) {
+ string extract_err;
+ if (!ExtractDeps(result, deps_type, deps_prefix, &deps_nodes,
+ &extract_err) &&
+ result->success()) {
+ if (!result->output.empty())
+ result->output.append("\n");
+ result->output.append(extract_err);
+ result->status = ExitFailure;
+ }
}
}
@@ -587,7 +598,7 @@
// Restat the edge outputs
TimeStamp output_mtime = 0;
- if (result->success() && !config_.dry_run) {
+ if (result->success() && !config_.dry_run && !phony_output) {
bool restat = edge->IsRestat();
vector<Node*> nodes_cleaned;
@@ -597,7 +608,7 @@
TimeStamp new_mtime = disk_interface_->LStat((*o)->path(), &is_dir, err);
if (new_mtime == -1)
return false;
- if (is_dir) {
+ if (is_dir && config_.uses_phony_outputs) {
if (!result->output.empty())
result->output.append("\n");
result->output.append("ninja: outputs should be files, not directories: ");
@@ -673,7 +684,7 @@
if (!rspfile.empty() && !g_keep_rsp)
disk_interface_->RemoveFile(rspfile);
- if (scan_.build_log()) {
+ if (scan_.build_log() && !phony_output) {
if (!scan_.build_log()->RecordCommand(edge, start_time_millis,
end_time_millis, output_mtime)) {
*err = string("Error writing to build log: ") + strerror(errno);
@@ -681,7 +692,7 @@
}
}
- if (!deps_type.empty() && !config_.dry_run) {
+ if (!deps_type.empty() && !config_.dry_run && !phony_output) {
Node* out = edge->outputs_[0];
TimeStamp deps_mtime = disk_interface_->LStat(out->path(), nullptr, err);
if (deps_mtime == -1)
diff --git a/src/build.h b/src/build.h
index 6cd2ae7..f800dab 100644
--- a/src/build.h
+++ b/src/build.h
@@ -144,6 +144,7 @@
failures_allowed(1), max_load_average(-0.0f),
frontend(NULL), frontend_file(NULL),
missing_depfile_should_err(false),
+ uses_phony_outputs(false),
output_directory_should_err(false) {}
enum Verbosity {
@@ -168,6 +169,10 @@
/// Whether a missing depfile should warn or print an error.
bool missing_depfile_should_err;
+ /// Whether the generator uses 'phony_output's
+ /// Controls the warnings below
+ bool uses_phony_outputs;
+
/// Whether an output can be a directory
bool output_directory_should_err;
};
diff --git a/src/build_test.cc b/src/build_test.cc
index ab6773b..4035658 100644
--- a/src/build_test.cc
+++ b/src/build_test.cc
@@ -2353,7 +2353,7 @@
ASSERT_EQ(1u, command_runner_.commands_ran_.size());
}
-TEST_F(BuildTest, OutputDirectoryWarning) {
+TEST_F(BuildTest, OutputDirectoryIgnored) {
ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
"rule mkdir\n"
" command = mkdir $out\n"
@@ -2365,7 +2365,29 @@
EXPECT_TRUE(builder_.Build(&err));
EXPECT_EQ("", err);
+ EXPECT_EQ("", status_.last_output_);
+}
+
+TEST_F(BuildTest, OutputDirectoryWarning) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule mkdir\n"
+" command = mkdir $out\n"
+"build outdir: mkdir\n"));
+
+ config_.uses_phony_outputs = true;
+
+ Builder builder(&state_, config_, NULL, NULL, &fs_, &status_, 0);
+ builder.command_runner_.reset(&command_runner_);
+
+ string err;
+ EXPECT_TRUE(builder.AddTarget("outdir", &err));
+ EXPECT_EQ("", err);
+ EXPECT_TRUE(builder.Build(&err));
+ EXPECT_EQ("", err);
+
EXPECT_EQ("ninja: outputs should be files, not directories: outdir", status_.last_output_);
+
+ builder.command_runner_.release();
}
TEST_F(BuildTest, OutputDirectoryError) {
@@ -2374,6 +2396,7 @@
" command = mkdir $out\n"
"build outdir: mkdir\n"));
+ config_.uses_phony_outputs = true;
config_.output_directory_should_err = true;
Builder builder(&state_, config_, NULL, NULL, &fs_, &status_, 0);
diff --git a/src/clean.cc b/src/clean.cc
index 351f1ec..d3c2d56 100644
--- a/src/clean.cc
+++ b/src/clean.cc
@@ -116,7 +116,7 @@
for (vector<Edge*>::iterator e = state_->edges_.begin();
e != state_->edges_.end(); ++e) {
// Do not try to remove phony targets
- if ((*e)->is_phony())
+ if ((*e)->is_phony() || (*e)->IsPhonyOutput())
continue;
// Do not remove generator's files unless generator specified.
if (!generator && (*e)->IsGenerator())
@@ -135,7 +135,7 @@
void Cleaner::DoCleanTarget(Node* target) {
if (Edge* e = target->in_edge()) {
// Do not try to remove phony targets
- if (!e->is_phony()) {
+ if (!e->is_phony() && !e->IsPhonyOutput()) {
Remove(target->path());
RemoveEdgeFiles(e);
}
@@ -209,6 +209,9 @@
for (vector<Edge*>::iterator e = state_->edges_.begin();
e != state_->edges_.end(); ++e) {
if ((*e)->rule().name() == rule->name()) {
+ if ((*e)->IsPhonyOutput()) {
+ continue;
+ }
for (vector<Node*>::iterator out_node = (*e)->outputs_.begin();
out_node != (*e)->outputs_.end(); ++out_node) {
Remove((*out_node)->path());
diff --git a/src/clean_test.cc b/src/clean_test.cc
index 395343b..4fdaa96 100644
--- a/src/clean_test.cc
+++ b/src/clean_test.cc
@@ -377,6 +377,35 @@
EXPECT_LT(0, fs_.Stat("phony", &err));
}
+TEST_F(CleanTest, CleanPhonyOutput) {
+ string err;
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule phony_out\n"
+" command = echo ${out}\n"
+" phony_output = true\n"
+"build phout: phony_out t1 t2\n"
+"build t1: cat\n"
+"build t2: cat\n"));
+
+ fs_.Create("phout", "");
+ fs_.Create("t1", "");
+ fs_.Create("t2", "");
+
+ // Check that CleanAll does not remove "phout".
+ Cleaner cleaner(&state_, config_, &fs_);
+ EXPECT_EQ(0, cleaner.CleanAll());
+ EXPECT_EQ(2, cleaner.cleaned_files_count());
+ EXPECT_LT(0, fs_.Stat("phout", &err));
+
+ fs_.Create("t1", "");
+ fs_.Create("t2", "");
+
+ // Check that CleanTarget does not remove "phony".
+ EXPECT_EQ(0, cleaner.CleanTarget("phout"));
+ EXPECT_EQ(2, cleaner.cleaned_files_count());
+ EXPECT_LT(0, fs_.Stat("phout", &err));
+}
+
TEST_F(CleanTest, CleanDepFileAndRspFileWithSpaces) {
ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
"rule cc_dep\n"
diff --git a/src/disk_interface_test.cc b/src/disk_interface_test.cc
index e968b78..07bd4db 100644
--- a/src/disk_interface_test.cc
+++ b/src/disk_interface_test.cc
@@ -213,7 +213,7 @@
struct StatTest : public StateTestWithBuiltinRules,
public DiskInterface {
- StatTest() : scan_(&state_, NULL, NULL, this) {}
+ StatTest() : scan_(&state_, NULL, NULL, this, false) {}
// DiskInterface implementation.
virtual TimeStamp Stat(const string& path, string* err) const;
diff --git a/src/eval_env.cc b/src/eval_env.cc
index 7b580a3..3d9f1c8 100644
--- a/src/eval_env.cc
+++ b/src/eval_env.cc
@@ -39,6 +39,7 @@
var == "restat" ||
var == "rspfile" ||
var == "rspfile_content" ||
+ var == "phony_output" ||
var == "msvc_deps_prefix";
}
diff --git a/src/graph.cc b/src/graph.cc
index ee9e629..78998ff 100644
--- a/src/graph.cc
+++ b/src/graph.cc
@@ -29,6 +29,9 @@
bool Node::PrecomputeStat(DiskInterface* disk_interface, std::string* err) {
if (in_edge() != nullptr) {
+ if (in_edge()->IsPhonyOutput()) {
+ return true;
+ }
return (precomputed_mtime_ = disk_interface->LStat(path_.str(), nullptr, err)) != -1;
} else {
return (precomputed_mtime_ = disk_interface->Stat(path_.str(), err)) != -1;
@@ -37,6 +40,7 @@
bool Node::Stat(DiskInterface* disk_interface, string* err) {
if (in_edge() != nullptr) {
+ assert(!in_edge()->IsPhonyOutput());
return (mtime_ = disk_interface->LStat(path_.str(), nullptr, err)) != -1;
} else {
return (mtime_ = disk_interface->Stat(path_.str(), err)) != -1;
@@ -190,22 +194,34 @@
stack->push_back(node);
bool dirty = false;
+ bool phony_output = edge->IsPhonyOutput();
edge->outputs_ready_ = true;
edge->deps_missing_ = false;
- // Load output mtimes so we can compare them to the most recent input below.
- for (vector<Node*>::iterator o = edge->outputs_.begin();
- o != edge->outputs_.end(); ++o) {
- if (!(*o)->StatIfNecessary(disk_interface_, err))
- return false;
- }
+ if (phony_output) {
+ EXPLAIN("edge with output %s is a phony output, so is always dirty",
+ node->path().c_str());
+ dirty = true;
- if (!dep_loader_.LoadDeps(edge, err)) {
- if (!err->empty())
+ if (edge->UsesDepsLog() || edge->UsesDepfile()) {
+ *err = "phony output " + node->path() + " has deps, which does not make sense.";
return false;
- // Failed to load dependency info: rebuild to regenerate it.
- // LoadDeps() did EXPLAIN() already, no need to do it here.
- dirty = edge->deps_missing_ = true;
+ }
+ } else {
+ // Load output mtimes so we can compare them to the most recent input below.
+ for (vector<Node*>::iterator o = edge->outputs_.begin();
+ o != edge->outputs_.end(); ++o) {
+ if (!(*o)->StatIfNecessary(disk_interface_, err))
+ return false;
+ }
+
+ if (!dep_loader_.LoadDeps(edge, err)) {
+ if (!err->empty())
+ return false;
+ // Failed to load dependency info: rebuild to regenerate it.
+ // LoadDeps() did EXPLAIN() already, no need to do it here.
+ dirty = edge->deps_missing_ = true;
+ }
}
// Visit all inputs; we're dirty if any of the inputs are dirty.
@@ -217,12 +233,19 @@
return false;
// If an input is not ready, neither are our outputs.
- if (Edge* in_edge = (*i)->in_edge()) {
+ Edge* in_edge = (*i)->in_edge();
+ if (in_edge != nullptr) {
if (!in_edge->outputs_ready_)
edge->outputs_ready_ = false;
}
- if (!edge->is_order_only(i - edge->inputs_.begin())) {
+ if (!phony_output && !edge->is_order_only(i - edge->inputs_.begin())) {
+ if (in_edge != nullptr && in_edge->IsPhonyOutput()) {
+ *err = "real file '" + node->path() +
+ "' depends on phony output '" + (*i)->path() + "'\n";
+ return false;
+ }
+
// If a regular input is dirty (or missing), we're dirty.
// Otherwise consider mtime.
if ((*i)->dirty()) {
@@ -307,10 +330,27 @@
bool DependencyScan::RecomputeOutputsDirty(Edge* edge, Node* most_recent_input,
bool* outputs_dirty, string* err) {
+ assert(!edge->IsPhonyOutput());
uint64_t command_hash = edge->GetCommandHash();
for (vector<Node*>::iterator o = edge->outputs_.begin();
o != edge->outputs_.end(); ++o) {
+ if (edge->is_phony()) {
+ // Phony edges don't write any output. Outputs are only dirty if
+ // there are no inputs and we're missing the output.
+ if (edge->inputs_.empty() && !(*o)->exists()) {
+ if (missing_phony_is_err_) {
+ *err = "output " + (*o)->path() + " of phony edge doesn't exist. Missing 'phony_output = true'?";
+ return false;
+ } else {
+ EXPLAIN("output %s of phony edge with no inputs doesn't exist",
+ (*o)->path().c_str());
+ *outputs_dirty = true;
+ return true;
+ }
+ }
+ continue;
+ }
if (RecomputeOutputDirty(edge, most_recent_input, command_hash, *o)) {
*outputs_dirty = true;
return true;
@@ -323,16 +363,7 @@
Node* most_recent_input,
uint64_t command_hash,
Node* output) {
- if (edge->is_phony()) {
- // Phony edges don't write any output. Outputs are only dirty if
- // there are no inputs and we're missing the output.
- if (edge->inputs_.empty() && !output->exists()) {
- EXPLAIN("output %s of phony edge with no inputs doesn't exist",
- output->path().c_str());
- return true;
- }
- return false;
- }
+ assert(!edge->is_phony());
BuildLog::LogEntry* entry = 0;
@@ -515,6 +546,7 @@
static const HashedStrView kRestat { "restat" };
static const HashedStrView kGenerator { "generator" };
static const HashedStrView kDeps { "deps" };
+static const HashedStrView kPhonyOutput { "phony_output" };
bool Edge::PrecomputeDepScanInfo(std::string* err) {
if (dep_scan_info_.valid)
@@ -529,10 +561,11 @@
*out = !value.empty();
return true;
};
- if (!get_bool_var(kRestat, EdgeEval::kShellEscape, &dep_scan_info_.restat)) return false;
- if (!get_bool_var(kGenerator, EdgeEval::kShellEscape, &dep_scan_info_.generator)) return false;
- if (!get_bool_var(kDeps, EdgeEval::kShellEscape, &dep_scan_info_.deps)) return false;
- if (!get_bool_var(kDepfile, EdgeEval::kDoNotEscape, &dep_scan_info_.depfile)) return false;
+ if (!get_bool_var(kRestat, EdgeEval::kShellEscape, &dep_scan_info_.restat)) return false;
+ if (!get_bool_var(kGenerator, EdgeEval::kShellEscape, &dep_scan_info_.generator)) return false;
+ if (!get_bool_var(kDeps, EdgeEval::kShellEscape, &dep_scan_info_.deps)) return false;
+ if (!get_bool_var(kDepfile, EdgeEval::kDoNotEscape, &dep_scan_info_.depfile)) return false;
+ if (!get_bool_var(kPhonyOutput, EdgeEval::kShellEscape, &dep_scan_info_.phony_output)) return false;
// Precompute the command hash.
std::string command;
diff --git a/src/graph.h b/src/graph.h
index 9e86793..b8637bf 100644
--- a/src/graph.h
+++ b/src/graph.h
@@ -290,6 +290,7 @@
bool generator = false;
bool deps = false;
bool depfile = false;
+ bool phony_output = false;
uint64_t command_hash = 0;
};
@@ -316,6 +317,7 @@
uint64_t GetCommandHash() { return ComputeDepScanInfo().command_hash; }
bool IsRestat() { return ComputeDepScanInfo().restat; }
bool IsGenerator() { return ComputeDepScanInfo().generator; }
+ bool IsPhonyOutput() { return ComputeDepScanInfo().phony_output; }
bool UsesDepsLog() { return ComputeDepScanInfo().deps; }
bool UsesDepfile() { return ComputeDepScanInfo().depfile; }
@@ -473,10 +475,11 @@
/// and updating the dirty/outputs_ready state of all the nodes and edges.
struct DependencyScan {
DependencyScan(State* state, BuildLog* build_log, DepsLog* deps_log,
- DiskInterface* disk_interface)
+ DiskInterface* disk_interface, bool missing_phony_is_err)
: build_log_(build_log),
disk_interface_(disk_interface),
- dep_loader_(state, deps_log, disk_interface) {}
+ dep_loader_(state, deps_log, disk_interface),
+ missing_phony_is_err_(missing_phony_is_err) {}
/// Used for tests.
bool RecomputeDirty(Node* node, std::string* err) {
@@ -529,6 +532,8 @@
BuildLog* build_log_;
DiskInterface* disk_interface_;
ImplicitDepLoader dep_loader_;
+
+ bool missing_phony_is_err_;
};
#endif // NINJA_GRAPH_H_
diff --git a/src/graph_test.cc b/src/graph_test.cc
index 18808b9..da495c8 100644
--- a/src/graph_test.cc
+++ b/src/graph_test.cc
@@ -18,7 +18,7 @@
#include "test.h"
struct GraphTest : public StateTestWithBuiltinRules {
- GraphTest() : scan_(&state_, NULL, NULL, &fs_) {}
+ GraphTest() : scan_(&state_, NULL, NULL, &fs_, false) {}
VirtualFileSystem fs_;
DependencyScan scan_;
@@ -447,6 +447,74 @@
EXPECT_TRUE(GetNode("out")->dirty());
}
+TEST_F(GraphTest, PhonyOutput) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule phony_out\n"
+" command = echo ${out}\n"
+" phony_output = true\n"
+"build foo: phony_out\n"));
+
+ Node* node = state_.LookupNode("foo");
+ Edge* edge = node->in_edge();
+ ASSERT_TRUE(edge->IsPhonyOutput());
+}
+
+TEST_F(GraphTest, PhonyOutputDependsOnPhonyOutput) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule phony_out\n"
+" command = echo ${out}\n"
+" phony_output = true\n"
+"build foo: phony_out\n"
+"build bar: phony_out foo\n"));
+
+ string err;
+ EXPECT_TRUE(scan_.RecomputeDirty(GetNode("bar"), &err));
+ ASSERT_EQ("", err);
+}
+
+TEST_F(GraphTest, RealDependsOnPhonyOutput) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule phony_out\n"
+" command = echo ${out}\n"
+" phony_output = true\n"
+"rule touch\n"
+" command = touch ${out}\n"
+"build foo: phony_out\n"
+"build bar: touch foo\n"));
+
+ string err;
+ EXPECT_FALSE(scan_.RecomputeDirty(GetNode("bar"), &err));
+ EXPECT_EQ("real file 'bar' depends on phony output 'foo'\n", err);
+}
+
+TEST_F(GraphTest, PhonyDependsOnPhonyOutput) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule phony_out\n"
+" command = echo ${out}\n"
+" phony_output = true\n"
+"build foo: phony_out\n"
+"build bar: phony foo\n"));
+
+ string err;
+ EXPECT_FALSE(scan_.RecomputeDirty(GetNode("bar"), &err));
+ EXPECT_EQ("real file 'bar' depends on phony output 'foo'\n", err);
+}
+
+TEST_F(GraphTest, MissingPhonyWithPhonyOutputs) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build foo: phony\n"));
+
+ string err;
+ EXPECT_TRUE(scan_.RecomputeDirty(GetNode("foo"), &err));
+ EXPECT_EQ("", err);
+ EXPECT_TRUE(GetNode("foo")->dirty());
+
+ state_.Reset();
+ DependencyScan scan(&state_, NULL, NULL, &fs_, true);
+ EXPECT_FALSE(scan.RecomputeDirty(GetNode("foo"), &err));
+ EXPECT_EQ("output foo of phony edge doesn't exist. Missing 'phony_output = true'?", err);
+}
+
TEST_F(GraphTest, DependencyCycle) {
AssertParse(&state_,
"build out: cat mid\n"
@@ -623,3 +691,18 @@
EXPECT_EQ("A", edge->pool()->name());
EXPECT_EQ("C", edge->GetBinding("pool"));
}
+
+TEST_F(GraphTest, PhonyOutputAlwaysDirty) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule phony_out\n"
+" command = echo ${out}\n"
+" phony_output = true\n"
+"build foo: phony_out\n"));
+
+ fs_.Create("foo", "");
+ string err;
+ EXPECT_TRUE(scan_.RecomputeDirty(GetNode("foo"), &err));
+ ASSERT_EQ("", err);
+
+ EXPECT_TRUE(GetNode("foo")->dirty());
+}
diff --git a/src/manifest_parser_test.cc b/src/manifest_parser_test.cc
index 57019d0..00f4aa4 100644
--- a/src/manifest_parser_test.cc
+++ b/src/manifest_parser_test.cc
@@ -62,6 +62,7 @@
" depfile = a\n"
" deps = a\n"
" description = a\n"
+" phony_output = a\n"
" generator = a\n"
" restat = a\n"
" rspfile = a\n"
diff --git a/src/ninja.cc b/src/ninja.cc
index 26692a1..fb5703d 100644
--- a/src/ninja.cc
+++ b/src/ninja.cc
@@ -989,6 +989,9 @@
" dupbuild={err,warn} multiple build lines for one target\n"
" phonycycle={err,warn} phony build statement references itself\n"
" missingdepfile={err,warn} how to treat missing depfiles\n"
+"\n"
+" usesphonyoutputs={yes,no} whether the generate uses 'phony_output's so \n"
+" that the following warnings work\n"
" outputdir={err,warn} how to treat outputs that are directories\n");
return false;
} else if (name == "dupbuild=err") {
@@ -1009,6 +1012,12 @@
} else if (name == "missingdepfile=warn") {
config->missing_depfile_should_err = false;
return true;
+ } else if (name == "usesphonyoutputs=yes") {
+ config->uses_phony_outputs = true;
+ return true;
+ } else if (name == "usesphonyoutputs=no") {
+ config->uses_phony_outputs = false;
+ return true;
} else if (name == "outputdir=err") {
config->output_directory_should_err = true;
return true;
@@ -1020,6 +1029,7 @@
SpellcheckString(name.c_str(), "dupbuild=err", "dupbuild=warn",
"phonycycle=err", "phonycycle=warn",
"missingdepfile=err", "missingdepfile=warn",
+ "usesphonyoutputs=yes", "usesphonyoutputs=no",
"outputdir=err", "outputdir=warn", NULL);
if (suggestion) {
Error("unknown warning flag '%s', did you mean '%s'?",