diff options
author | Jingwen Chen <jingwen@google.com> | 2020-09-21 13:46:37 +0000 |
---|---|---|
committer | Jingwen Chen <jingwen@google.com> | 2020-10-01 08:44:07 +0000 |
commit | 4b3e54d29302e0accfab963fc1af21b2d44771d1 (patch) | |
tree | 7000047a495858d78c78951094e12495cfd719a5 | |
parent | ffd6a3e01d9cf37cc48c966af361eabacedf500b (diff) | |
download | ninja-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.cc | 82 | ||||
-rw-r--r-- | src/build.h | 9 | ||||
-rw-r--r-- | src/build_test.cc | 234 | ||||
-rw-r--r-- | src/deps_log.cc | 2 | ||||
-rw-r--r-- | src/disk_interface.cc | 7 | ||||
-rw-r--r-- | src/disk_interface.h | 10 | ||||
-rw-r--r-- | src/disk_interface_test.cc | 8 | ||||
-rw-r--r-- | src/eval_env.cc | 1 | ||||
-rw-r--r-- | src/graph.cc | 14 | ||||
-rw-r--r-- | src/graph.h | 4 | ||||
-rw-r--r-- | src/ninja.cc | 26 | ||||
-rw-r--r-- | src/test.cc | 4 | ||||
-rw-r--r-- | src/test.h | 2 |
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; } @@ -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); |