aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJingwen Chen <jingwen@google.com>2020-09-21 13:46:37 +0000
committerJingwen Chen <jingwen@google.com>2020-10-01 08:44:07 +0000
commit4b3e54d29302e0accfab963fc1af21b2d44771d1 (patch)
tree7000047a495858d78c78951094e12495cfd719a5
parentffd6a3e01d9cf37cc48c966af361eabacedf500b (diff)
downloadninja-4b3e54d29302e0accfab963fc1af21b2d44771d1.tar.gz
Add support for new 'symlink_outputs' reserved variable to AOSP Ninja statements.
This variable contains a space-separated list of paths representing declared symlink outputs that an edge creates. If `-o usessymlinkoutputs=yes`, Ninja will check that symlink outputs are in this list, and file outputs are not in this list. Otherwise, it prints a warning. Defaults to `usesymlinkoutputs=no`, which does not affect existing Ninja files (unless symlink_outputs is already being used, which isn't the case in AOSP). `-w undeclaredsymlinkoutputs=err` turns that warning into error. This is not necessary today because AOSP Ninja (not upstream Ninja)runs `lstat` on all outputs, which would return the correct metadata regardless if the output is a symlink or a file. However, tooling integration with Ninja files require symlink outputs to be marked as such, and aggregating them in the symlink_outputs variable is probably the least invasive approach. Test: (in build-tools) OUT_DIR=out build/soong/soong_ui.bash --make-mode --skip-make ninja ninja_test && out/soong/host/linux-x86/nativetest64/ninja_test/ninja_test Test: (in AOSP) m NINJA_ARGS="-o usessymlinkoutputs=yes" Test: (in AOSP) m NINJA_ARGS="-o usessymlinkoutputs=yes -w undeclaredsymlinkoutputs=err" Bug: 160568334 Change-Id: Iae69ccb6014cace9ab6e61e0b6aca00f6d6ac8c6
-rw-r--r--src/build.cc82
-rw-r--r--src/build.h9
-rw-r--r--src/build_test.cc234
-rw-r--r--src/deps_log.cc2
-rw-r--r--src/disk_interface.cc7
-rw-r--r--src/disk_interface.h10
-rw-r--r--src/disk_interface_test.cc8
-rw-r--r--src/eval_env.cc1
-rw-r--r--src/graph.cc14
-rw-r--r--src/graph.h4
-rw-r--r--src/ninja.cc26
-rw-r--r--src/test.cc4
-rw-r--r--src/test.h2
13 files changed, 379 insertions, 24 deletions
diff --git a/src/build.cc b/src/build.cc
index 1ee7603..5bcc5de 100644
--- a/src/build.cc
+++ b/src/build.cc
@@ -16,9 +16,12 @@
#include <assert.h>
#include <errno.h>
+#include <functional>
+#include <set>
+#include <sstream>
#include <stdio.h>
#include <stdlib.h>
-#include <functional>
+#include <vector>
#ifdef _WIN32
#include <fcntl.h>
@@ -559,7 +562,7 @@ void Builder::Cleanup() {
// but is interrupted before it touches its output file.)
string err;
bool is_dir = false;
- TimeStamp new_mtime = disk_interface_->LStat((*o)->path(), &is_dir, &err);
+ TimeStamp new_mtime = disk_interface_->LStat((*o)->path(), &is_dir, nullptr, &err);
if (new_mtime == -1) // Log and ignore LStat() errors.
status_->Error("%s", err.c_str());
if (!is_dir && (!depfile.empty() || (*o)->mtime() != new_mtime))
@@ -815,13 +818,69 @@ bool Builder::FinishCommand(CommandRunner::Result* result, string* err) {
}
}
+ set<string> declared_symlinks;
+ if (config_.uses_symlink_outputs) {
+ string symlink_outputs = edge->GetSymlinkOutputs();
+ if (symlink_outputs.length() > 0) {
+ stringstream ss(symlink_outputs);
+ string path;
+ /// Naively split symlink_outputs path by the empty ' ' space character.
+ /// because the '$ ' escape doesn't exist at this stage. In experimentation
+ /// and practice across a number of AOSP configurations, this is OK.
+ ///
+ /// We could modify the GetBindingImpl/GetSymlinkOutputs API to support lists,
+ /// but it'd be an invasive change that'll require a little bit more designing.
+ /// For example, how do we expand "${out}.d" if ${out} is a list?
+ ///
+ /// That said, keep in mind that this is a simple string split that could
+ /// fail with paths containing spaces.
+ while (getline(ss, path, ' ')) {
+ uint64_t slash_bits;
+ if (!CanonicalizePath(&path, &slash_bits, err)) {
+ return false;
+ }
+ declared_symlinks.insert(move(path));
+ }
+ }
+ }
+
for (vector<Node*>::iterator o = edge->outputs_.begin();
o != edge->outputs_.end(); ++o) {
bool is_dir = false;
+ bool is_symlink = false;
TimeStamp old_mtime = (*o)->mtime();
- if (!(*o)->LStat(disk_interface_, &is_dir, err))
+ if (!(*o)->LStat(disk_interface_, &is_dir, &is_symlink, err))
return false;
+
TimeStamp new_mtime = (*o)->mtime();
+
+ if (config_.uses_symlink_outputs) {
+ /// Warn or error if created symlinks aren't declared in symlink_outputs,
+ /// or if created files are declared in symlink_outputs.
+ string path = (*o)->path();
+ if (is_symlink) {
+ if (declared_symlinks.find(path) == declared_symlinks.end()) {
+ // Not in declared_symlinks
+ if (!result->output.empty())
+ result->output.append("\n");
+ result->output.append("ninja: " + path + " is a symlink, but it was not declared in symlink_outputs");
+ if (config_.undeclared_symlink_outputs_should_err) {
+ result->status = ExitFailure;
+ }
+ } else {
+ declared_symlinks.erase(path);
+ }
+ } else if (!is_symlink && declared_symlinks.find(path) != declared_symlinks.end()) {
+ if (!result->output.empty())
+ result->output.append("\n");
+ result->output.append("ninja: " + path + " is not a symlink, but it was declared in symlink_outputs");
+ declared_symlinks.erase(path);
+ if (config_.undeclared_symlink_outputs_should_err) {
+ result->status = ExitFailure;
+ }
+ }
+ }
+
if (config_.uses_phony_outputs) {
if (new_mtime == 0) {
if (!result->output.empty())
@@ -860,6 +919,21 @@ bool Builder::FinishCommand(CommandRunner::Result* result, string* err) {
}
}
+ /// Ensure that declared_symlinks is empty after verifying that symlink outputs
+ /// were declared in the edge. A non-empty declared_symlinks set indicates that
+ /// not all declared symlinks were created by the edge itself (over-specification).
+ if (config_.uses_symlink_outputs && declared_symlinks.size() > 0) {
+ string missing_outputs;
+ for (string symlink : declared_symlinks) {
+ missing_outputs = missing_outputs + " " + symlink;
+ }
+ result->output.append(
+ "ninja: not all symlink_outputs were created for this edge:" + missing_outputs);
+ if (config_.undeclared_symlink_outputs_should_err) {
+ result->status = ExitFailure;
+ }
+ }
+
status_->BuildEdgeFinished(edge, end_time_millis, result);
if (result->success() && !nodes_cleaned.empty()) {
@@ -918,7 +992,7 @@ bool Builder::FinishCommand(CommandRunner::Result* result, string* err) {
if (!deps_type.empty() && !config_.dry_run && !phony_output) {
Node* out = edge->outputs_[0];
- TimeStamp deps_mtime = disk_interface_->LStat(out->path(), nullptr, err);
+ TimeStamp deps_mtime = disk_interface_->LStat(out->path(), nullptr, nullptr, err);
if (deps_mtime == -1)
return false;
if (!scan_.deps_log()->RecordDeps(out, deps_mtime, deps_nodes)) {
diff --git a/src/build.h b/src/build.h
index 61d3f78..45938ad 100644
--- a/src/build.h
+++ b/src/build.h
@@ -166,6 +166,8 @@ struct BuildConfig {
failures_allowed(1), max_load_average(-0.0f),
frontend(NULL), frontend_file(NULL),
missing_depfile_should_err(false),
+ uses_symlink_outputs(false),
+ undeclared_symlink_outputs_should_err(false),
uses_phony_outputs(false),
output_directory_should_err(false),
missing_output_file_should_err(false),
@@ -195,6 +197,13 @@ struct BuildConfig {
/// Whether a missing depfile should warn or print an error.
bool missing_depfile_should_err;
+ /// Whether Ninja should check that symlink outputs are declared in the
+ /// symlink_outputs variable
+ bool uses_symlink_outputs;
+
+ /// Whether undeclared symlink outputs should print a warning or error out
+ bool undeclared_symlink_outputs_should_err;
+
/// Whether the generator uses 'phony_output's
/// Controls the warnings below
bool uses_phony_outputs;
diff --git a/src/build_test.cc b/src/build_test.cc
index 6b8e178..e8faf9d 100644
--- a/src/build_test.cc
+++ b/src/build_test.cc
@@ -651,6 +651,14 @@ bool FakeCommandRunner::StartCommand(Edge* edge) {
if (fs_->ReadFile(edge->inputs_[0]->path(), &content, &err) ==
DiskInterface::Okay)
fs_->WriteFile(edge->outputs_[0]->path(), content);
+ } else if (edge->rule().name() == "symlink") {
+ assert(edge->inputs_.size() == 1);
+ assert(edge->outputs_.size() == 1);
+ fs_->CreateSymlink(edge->outputs_[0]->path(), edge->inputs_[0]->path());
+ } else if (edge->rule().name() == "dangling_symlink") {
+ assert(edge->inputs_.empty());
+ assert(edge->outputs_.size() == 1);
+ fs_->CreateSymlink(edge->outputs_[0]->path(), "nil");
} else {
printf("unknown command\n");
return false;
@@ -845,6 +853,232 @@ TEST_F(BuildTest, ImplicitOutput) {
EXPECT_EQ("touch out out.imp", command_runner_.commands_ran_[0]);
}
+TEST_F(BuildTest, SymlinkOutputsIsValidVariable) {
+ string err;
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule dangling_symlink\n"
+" command = ln -sf nil $out\n"
+" symlink_outputs = $out\n"
+"build l1: dangling_symlink\n"
+"rule symlink\n"
+" command = ln -sf $in $out\n"
+" symlink_outputs = $out\n"
+"build l2: symlink file\n"
+))
+
+ fs_.Create("file", "content");
+ /// Disabled, but symlink_outputs is still a valid variable.
+ config_.uses_symlink_outputs = false;
+
+ EXPECT_TRUE(builder_.AddTarget("l1", &err));
+ EXPECT_TRUE(builder_.AddTarget("l2", &err));
+ EXPECT_TRUE(builder_.Build(&err));
+ EXPECT_EQ("", err);
+ EXPECT_EQ(2u, command_runner_.commands_ran_.size());
+}
+
+TEST_F(BuildTest, SymlinkOutputsOKWithDeclaration) {
+ string err;
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule dangling_symlink\n"
+" command = ln -sf nil $out\n"
+" symlink_outputs = $out\n"
+"build l1: dangling_symlink\n"
+"rule symlink\n"
+" command = ln -sf $in $out\n"
+" symlink_outputs = $out\n"
+"build l2: symlink file\n"
+))
+
+ config_.uses_symlink_outputs = true;
+ config_.undeclared_symlink_outputs_should_err = true;
+
+ fs_.Create("file", "content");
+
+ EXPECT_TRUE(builder_.AddTarget("l1", &err));
+ EXPECT_TRUE(builder_.AddTarget("l2", &err));
+ EXPECT_TRUE(builder_.Build(&err));
+ EXPECT_EQ("", err);
+ EXPECT_EQ(2u, command_runner_.commands_ran_.size());
+}
+
+TEST_F(BuildTest, SymlinkOutputsOKWithUncanonicalizedDeclaration) {
+ string err;
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule dangling_symlink\n"
+" command = ln -sf nil .//$out\n"
+" symlink_outputs = ././$out\n"
+"build l1: dangling_symlink\n"
+"rule symlink\n"
+" command = ln -sf $in ././$out\n"
+" symlink_outputs = .//$out\n"
+"build l2: symlink file\n"
+))
+
+ config_.uses_symlink_outputs = true;
+ config_.undeclared_symlink_outputs_should_err = true;
+
+ fs_.Create("file", "content");
+
+ EXPECT_TRUE(builder_.AddTarget("l1", &err));
+ EXPECT_TRUE(builder_.AddTarget("l2", &err));
+ EXPECT_TRUE(builder_.Build(&err));
+ EXPECT_EQ("", err);
+ EXPECT_EQ(2u, command_runner_.commands_ran_.size());
+}
+
+TEST_F(BuildTest, FileOutputsWarnWithSymlinkOutputsDeclaration) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule touch\n"
+" command = touch $out\n"
+" symlink_outputs = $out\n"
+"build f1: touch\n"));
+
+ config_.uses_symlink_outputs = true;
+ config_.undeclared_symlink_outputs_should_err = false;
+
+ string err;
+ EXPECT_TRUE(builder_.AddTarget("f1", &err));
+ EXPECT_EQ("", err);
+
+ EXPECT_TRUE(builder_.Build(&err));
+ EXPECT_EQ("ninja: f1 is not a symlink, but it was declared in symlink_outputs", status_.last_output_);
+}
+
+TEST_F(BuildTest, FileOutputsErrorWithSymlinkOutputsDeclaration) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule touch\n"
+" command = touch $out\n"
+" symlink_outputs = $out\n"
+"build f1: touch\n"));
+
+ config_.uses_symlink_outputs = true;
+ config_.undeclared_symlink_outputs_should_err = true;
+
+ string err;
+ EXPECT_TRUE(builder_.AddTarget("f1", &err));
+ EXPECT_EQ("", err);
+
+ EXPECT_FALSE(builder_.Build(&err));
+ EXPECT_EQ("subcommand failed", err);
+ EXPECT_EQ("ninja: f1 is not a symlink, but it was declared in symlink_outputs", status_.last_output_);
+}
+
+TEST_F(BuildTest, DanglingSymlinkOutputsWarnWithoutDeclaration) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule dangling_symlink\n"
+" command = ln -sf nil $out\n"
+"build l1: dangling_symlink\n"
+))
+
+ config_.uses_symlink_outputs = true;
+ config_.undeclared_symlink_outputs_should_err = false;
+
+ string err;
+ EXPECT_TRUE(builder_.AddTarget("l1", &err));
+ EXPECT_EQ("", err);
+
+ EXPECT_TRUE(builder_.Build(&err));
+ EXPECT_EQ("ninja: l1 is a symlink, but it was not declared in symlink_outputs", status_.last_output_);
+}
+
+TEST_F(BuildTest, RegularSymlinkOutputsWarnWithoutDeclaration) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule symlink\n"
+" command = ln -sf $in $out\n"
+"build l1: symlink file\n"
+))
+ fs_.Create("file", "content");
+
+ config_.uses_symlink_outputs = true;
+ config_.undeclared_symlink_outputs_should_err = false;
+
+ string err;
+ EXPECT_TRUE(builder_.AddTarget("l1", &err));
+ EXPECT_EQ("", err);
+
+ EXPECT_TRUE(builder_.Build(&err));
+ EXPECT_EQ("ninja: l1 is a symlink, but it was not declared in symlink_outputs", status_.last_output_);
+}
+
+TEST_F(BuildTest, DanglingSymlinkOutputsErrorWithoutDeclaration) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule dangling_symlink\n"
+" command = ln -sf nil $out\n"
+"build l1: dangling_symlink\n"
+))
+
+ config_.uses_symlink_outputs = true;
+ config_.undeclared_symlink_outputs_should_err = true;
+
+ string err;
+ EXPECT_TRUE(builder_.AddTarget("l1", &err));
+ EXPECT_EQ("", err);
+
+ EXPECT_FALSE(builder_.Build(&err));
+ EXPECT_EQ("subcommand failed", err);
+ EXPECT_EQ("ninja: l1 is a symlink, but it was not declared in symlink_outputs", status_.last_output_);
+}
+
+TEST_F(BuildTest, RegularSymlinkOutputsErrorWithoutDeclaration) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule symlink\n"
+" command = ln -sf $in $out\n"
+"build l1: symlink file\n"
+))
+ fs_.Create("file", "content");
+
+ config_.uses_symlink_outputs = true;
+ config_.undeclared_symlink_outputs_should_err = true;
+
+ string err;
+ EXPECT_TRUE(builder_.AddTarget("l1", &err));
+ EXPECT_EQ("", err);
+
+ EXPECT_FALSE(builder_.Build(&err));
+ EXPECT_EQ("subcommand failed", err);
+ EXPECT_EQ("ninja: l1 is a symlink, but it was not declared in symlink_outputs", status_.last_output_);
+}
+
+TEST_F(BuildTest, ExtraSymlinkOutputsPrintsWarning) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule dangling_symlink\n"
+" command = ln -sf nil $out\n"
+"build l1: dangling_symlink\n"
+" symlink_outputs = l1 l2\n"
+))
+
+ config_.uses_symlink_outputs = true;
+ config_.undeclared_symlink_outputs_should_err = false;
+
+ string err;
+ EXPECT_TRUE(builder_.AddTarget("l1", &err));
+ EXPECT_EQ("", err);
+
+ EXPECT_TRUE(builder_.Build(&err));
+ EXPECT_EQ("ninja: not all symlink_outputs were created for this edge: l2", status_.last_output_);
+}
+
+TEST_F(BuildTest, ExtraSymlinkOutputsRaisesError) {
+ ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule dangling_symlink\n"
+" command = ln -sf nil $out\n"
+"build l1: dangling_symlink\n"
+" symlink_outputs = l1 l2\n"
+))
+
+ config_.uses_symlink_outputs = true;
+ config_.undeclared_symlink_outputs_should_err = true;
+
+ string err;
+ EXPECT_TRUE(builder_.AddTarget("l1", &err));
+ EXPECT_EQ("", err);
+
+ EXPECT_FALSE(builder_.Build(&err));
+ EXPECT_EQ("subcommand failed", err);
+ EXPECT_EQ("ninja: not all symlink_outputs were created for this edge: l2", status_.last_output_);
+}
+
// Test case from
// https://github.com/ninja-build/ninja/issues/148
TEST_F(BuildTest, MultiOutIn) {
diff --git a/src/deps_log.cc b/src/deps_log.cc
index f2a3888..de7a3cd 100644
--- a/src/deps_log.cc
+++ b/src/deps_log.cc
@@ -709,7 +709,7 @@ bool DepsLog::Recompact(const string& path, const DiskInterface& disk, string* e
// If the current manifest does not define this edge, skip if it's missing
// from the disk.
string err;
- TimeStamp mtime = disk.LStat(node->path(), nullptr, &err);
+ TimeStamp mtime = disk.LStat(node->path(), nullptr, nullptr, &err);
if (mtime == -1)
Error("%s", err.c_str()); // log and ignore LStat() errors
if (mtime == 0)
diff --git a/src/disk_interface.cc b/src/disk_interface.cc
index 0044c5b..4a5ea34 100644
--- a/src/disk_interface.cc
+++ b/src/disk_interface.cc
@@ -229,7 +229,8 @@ TimeStamp RealDiskInterface::Stat(const string& path, string* err) const {
#endif
}
-TimeStamp RealDiskInterface::LStat(const string& path, bool* is_dir, string* err) const {
+TimeStamp RealDiskInterface::LStat(
+ const string& path, bool* is_dir, bool* is_symlink, string* err) const {
METRIC_RECORD("node lstat");
#ifdef _WIN32
#error unimplemented
@@ -244,6 +245,10 @@ TimeStamp RealDiskInterface::LStat(const string& path, bool* is_dir, string* err
if (is_dir != nullptr) {
*is_dir = S_ISDIR(st.st_mode);
}
+
+ if (is_symlink != nullptr) {
+ *is_symlink = S_ISLNK(st.st_mode);
+ }
return StatTimestamp(st);
#endif
}
diff --git a/src/disk_interface.h b/src/disk_interface.h
index f8b1b0d..66083c6 100644
--- a/src/disk_interface.h
+++ b/src/disk_interface.h
@@ -107,11 +107,11 @@ struct DiskInterface: public FileReader {
/// other errors. Thread-safe iff IsStatThreadSafe returns true.
virtual TimeStamp Stat(const string& path, string* err) const = 0;
- /// lstat() a path, returning the mtime, or 0 if missing and 01 on
+ /// lstat() a path, returning the mtime, or 0 if missing and -1 on
/// other errors. Does not traverse symlinks, and returns whether the
- /// path represents a directory. Thread-safe iff IsStatThreadSafe
- /// returns true.
- virtual TimeStamp LStat(const string& path, bool* is_dir, string* err) const = 0;
+ /// path represents a directory or a symlink. Thread-safe iff
+ /// IsStatThreadSafe returns true.
+ virtual TimeStamp LStat(const string& path, bool* is_dir, bool* is_symlink, string* err) const = 0;
/// True if Stat() can be called from multiple threads concurrently.
virtual bool IsStatThreadSafe() const = 0;
@@ -144,7 +144,7 @@ struct RealDiskInterface : public DiskInterface {
{}
virtual ~RealDiskInterface() {}
virtual TimeStamp Stat(const string& path, string* err) const;
- virtual TimeStamp LStat(const string& path, bool* is_dir, string* err) const;
+ virtual TimeStamp LStat(const string& path, bool* is_dir, bool* is_symlink, string* err) const;
virtual bool IsStatThreadSafe() const;
virtual bool MakeDir(const string& path);
virtual bool WriteFile(const string& path, const string& contents);
diff --git a/src/disk_interface_test.cc b/src/disk_interface_test.cc
index 090adc4..fe8ab73 100644
--- a/src/disk_interface_test.cc
+++ b/src/disk_interface_test.cc
@@ -217,7 +217,7 @@ struct StatTest : public StateTestWithBuiltinRules,
// DiskInterface implementation.
virtual TimeStamp Stat(const string& path, string* err) const;
- virtual TimeStamp LStat(const string& path, bool* is_dir, string* err) const;
+ virtual TimeStamp LStat(const string& path, bool* is_dir, bool* is_symlink, string* err) const;
virtual bool IsStatThreadSafe() const;
virtual bool WriteFile(const string& path, const string& contents) {
assert(false);
@@ -248,16 +248,18 @@ struct StatTest : public StateTestWithBuiltinRules,
};
TimeStamp StatTest::Stat(const string& path, string* err) const {
- return LStat(path, nullptr, err);
+ return LStat(path, nullptr, nullptr, err);
}
-TimeStamp StatTest::LStat(const string& path, bool* is_dir, string* err) const {
+TimeStamp StatTest::LStat(const string& path, bool* is_dir, bool* is_symlink, string* err) const {
stats_.push_back(path);
map<string, TimeStamp>::const_iterator i = mtimes_.find(path);
if (i == mtimes_.end())
return 0; // File not found.
if (is_dir != nullptr)
*is_dir = false;
+ if (is_symlink != nullptr)
+ *is_symlink = false;
return i->second;
}
diff --git a/src/eval_env.cc b/src/eval_env.cc
index b52436b..0a30e66 100644
--- a/src/eval_env.cc
+++ b/src/eval_env.cc
@@ -41,6 +41,7 @@ bool Rule::IsReservedBinding(StringPiece var) {
var == "rspfile" ||
var == "rspfile_content" ||
var == "phony_output" ||
+ var == "symlink_outputs" ||
var == "msvc_deps_prefix";
}
diff --git a/src/graph.cc b/src/graph.cc
index 0dfcda2..747784e 100644
--- a/src/graph.cc
+++ b/src/graph.cc
@@ -33,7 +33,7 @@ bool Node::PrecomputeStat(DiskInterface* disk_interface, std::string* err) {
if (in_edge()->IsPhonyOutput()) {
return true;
}
- return (precomputed_mtime_ = disk_interface->LStat(path_.str(), nullptr, err)) != -1;
+ return (precomputed_mtime_ = disk_interface->LStat(path_.str(), nullptr, nullptr, err)) != -1;
} else {
return (precomputed_mtime_ = disk_interface->Stat(path_.str(), err)) != -1;
}
@@ -42,16 +42,17 @@ bool Node::PrecomputeStat(DiskInterface* disk_interface, std::string* err) {
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;
+ return (mtime_ = disk_interface->LStat(path_.str(), nullptr, nullptr, err)) != -1;
} else {
return (mtime_ = disk_interface->Stat(path_.str(), err)) != -1;
}
}
-bool Node::LStat(DiskInterface* disk_interface, bool* is_dir, string* err) {
+bool Node::LStat(
+ DiskInterface* disk_interface, bool* is_dir, bool* is_symlink, string* err) {
assert(in_edge() != nullptr);
assert(!in_edge()->IsPhonyOutput());
- return (mtime_ = disk_interface->LStat(path_.str(), is_dir, err)) != -1;
+ return (mtime_ = disk_interface->LStat(path_.str(), is_dir, is_symlink, err)) != -1;
}
bool DependencyScan::RecomputeNodesDirty(const std::vector<Node*>& initial_nodes,
@@ -603,6 +604,7 @@ static const HashedStrView kDepfile { "depfile" };
static const HashedStrView kDyndep { "dyndep" };
static const HashedStrView kRspfile { "rspfile" };
static const HashedStrView kRspFileContent { "rspfile_content" };
+static const HashedStrView kSymlinkOutputs { "symlink_outputs" };
bool Edge::EvaluateCommand(std::string* out_append, bool incl_rsp_file,
std::string* err) {
@@ -699,6 +701,10 @@ std::string Edge::GetBinding(const HashedStrView& key) {
return GetBindingImpl(key, EdgeEval::kFinalScope, EdgeEval::kShellEscape);
}
+std::string Edge::GetSymlinkOutputs() {
+ return GetBindingImpl(kSymlinkOutputs, EdgeEval::kFinalScope, EdgeEval::kDoNotEscape);
+}
+
std::string Edge::GetUnescapedDepfile() {
return GetBindingImpl(kDepfile, EdgeEval::kFinalScope, EdgeEval::kDoNotEscape);
}
diff --git a/src/graph.h b/src/graph.h
index 776c1ac..7028912 100644
--- a/src/graph.h
+++ b/src/graph.h
@@ -117,7 +117,7 @@ struct Node {
bool Stat(DiskInterface* disk_interface, string* err);
/// Only use when lstat() is desired (output files)
- bool LStat(DiskInterface* disk_interface, bool* is_dir, string* err);
+ bool LStat(DiskInterface* disk_interface, bool* is_dir, bool* is_symlink, string* err);
/// Return false on error.
bool StatIfNecessary(DiskInterface* disk_interface, string* err) {
@@ -373,6 +373,8 @@ public:
/// Like GetBinding("rspfile"), but without shell escaping.
string GetUnescapedRspfile();
+ string GetSymlinkOutputs();
+
void Dump(const char* prefix="") const;
/// Temporary fields used only during manifest parsing.
diff --git a/src/ninja.cc b/src/ninja.cc
index 65cc8aa..375e50d 100644
--- a/src/ninja.cc
+++ b/src/ninja.cc
@@ -181,7 +181,7 @@ struct NinjaMain : public BuildLogUser {
// Do keep entries around for files which still exist on disk, for
// generators that want to use this information.
string err;
- TimeStamp mtime = disk_interface_.LStat(s.AsString(), nullptr, &err);
+ TimeStamp mtime = disk_interface_.LStat(s.AsString(), nullptr, nullptr, &err);
if (mtime == -1)
Error("%s", err.c_str()); // Log and ignore Stat() errors.
return mtime == 0;
@@ -1132,7 +1132,11 @@ bool WarningEnable(const string& name, Options* options, BuildConfig* config) {
" requires -o usesphonyoutputs=yes\n"
" outputdir={err,warn} how to treat outputs that are directories\n"
" missingoutfile={err,warn} how to treat missing output files\n"
-" oldoutput={err,warn} how to treat output files older than their inputs\n");
+" oldoutput={err,warn} how to treat output files older than their inputs\n"
+"\n"
+" requires -o usessymlinkoutputs=yes\n"
+" undeclaredsymlinkoutputs={err,warn} build statements creating symlink outputs must "
+"declare them in symlink_outputs\n");
return false;
} else if (name == "dupbuild=err") {
options->dupe_edges_should_err = true;
@@ -1176,6 +1180,12 @@ bool WarningEnable(const string& name, Options* options, BuildConfig* config) {
} else if (name == "oldoutput=warn") {
config->old_output_should_err = false;
return true;
+ } else if (name == "undeclaredsymlinkoutputs=err") {
+ config->undeclared_symlink_outputs_should_err = true;
+ return true;
+ } else if (name == "undeclaredsymlinkoutputs=warn") {
+ config->undeclared_symlink_outputs_should_err = false;
+ return true;
} else {
const char* suggestion =
SpellcheckString(name.c_str(), "dupbuild=err", "dupbuild=warn",
@@ -1183,7 +1193,8 @@ bool WarningEnable(const string& name, Options* options, BuildConfig* config) {
"missingdepfile=err", "missingdepfile=warn",
"outputdir=err", "outputdir=warn",
"missingoutfile=err", "missingoutfile=warn",
- "oldoutput=err", "oldoutput=warn", NULL);
+ "oldoutput=err", "oldoutput=warn",
+ "undeclaredsymlinkoutputs=err", "undeclaredsymlinkoutputs=warn", NULL);
if (suggestion) {
Error("unknown warning flag '%s', did you mean '%s'?",
name.c_str(), suggestion);
@@ -1204,6 +1215,9 @@ bool OptionEnable(const string& name, Options* options, BuildConfig* config) {
" outputdir\n"
" missingoutfile\n"
" oldoutput\n"
+" usessymlinkoutputs={yes,no} whether the generate uses 'symlink_outputs' so \n"
+" that these warnings work:\n"
+" undeclaredsymlinkoutputs\n"
" preremoveoutputs={yes,no} whether to remove outputs before running rule\n");
return false;
} else if (name == "usesphonyoutputs=yes") {
@@ -1212,6 +1226,12 @@ bool OptionEnable(const string& name, Options* options, BuildConfig* config) {
} else if (name == "usesphonyoutputs=no") {
config->uses_phony_outputs = false;
return true;
+ } else if (name == "usessymlinkoutputs=yes") {
+ config->uses_symlink_outputs = true;
+ return true;
+ } else if (name == "usessymlinkoutputs=no") {
+ config->uses_symlink_outputs = false;
+ return true;
} else if (name == "preremoveoutputs=yes") {
config->pre_remove_output_files = true;
return true;
diff --git a/src/test.cc b/src/test.cc
index 86983cc..7cacae4 100644
--- a/src/test.cc
+++ b/src/test.cc
@@ -200,7 +200,7 @@ TimeStamp VirtualFileSystem::Stat(const string& path, string* err) const {
return 0;
}
-TimeStamp VirtualFileSystem::LStat(const string& path, bool* is_dir, string* err) const {
+TimeStamp VirtualFileSystem::LStat(const string& path, bool* is_dir, bool* is_symlink, string* err) const {
DirMap::const_iterator d = dirs_.find(path);
if (d != dirs_.end()) {
if (is_dir != nullptr)
@@ -212,6 +212,8 @@ TimeStamp VirtualFileSystem::LStat(const string& path, bool* is_dir, string* err
if (i != files_.end()) {
if (is_dir != nullptr)
*is_dir = false;
+ if (is_symlink != nullptr)
+ *is_symlink = i->second.is_symlink;
*err = i->second.stat_error;
return i->second.mtime;
}
diff --git a/src/test.h b/src/test.h
index 5d49b0a..4adc3c9 100644
--- a/src/test.h
+++ b/src/test.h
@@ -148,7 +148,7 @@ struct VirtualFileSystem : public DiskInterface {
// DiskInterface
virtual TimeStamp Stat(const string& path, string* err) const;
- virtual TimeStamp LStat(const string& path, bool* is_dir, string* err) const;
+ virtual TimeStamp LStat(const string& path, bool* is_dir, bool* is_symlink, string* err) const;
virtual bool IsStatThreadSafe() const;
virtual bool WriteFile(const string& path, const string& contents);
virtual bool MakeDir(const string& path);