Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions doc/manual.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,28 @@ express the implicit dependency.)
File paths are compared as is, which means that an absolute path and a
relative path, pointing to the same file, are considered different by Ninja.

[[dir_inputs]]
Directory inputs
~~~~~~~~~~~~~~~~

_Available since Ninja 1.14._

If an input path on a build line ends with a path separator (`/`), the input is
treated as a directory rather than a regular file. When Ninja checks whether the
build edge is up-to-date, it stats the directory itself and compares its
modification time against the outputs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this phrase is confusing because Ninja already stats directory paths if there is no trailing slash. And I am sure that someone is using this in their build system, even if it is a fragile thing to do, so this behavior cannot be changed lightly.

A better phrasing might explain what the trailing slash does, e.g.:

"""
An input path with a trailing separator (e.g. foo/bar/) must always point to a valid directory, otherwise Ninja will complain with an error. By contrast, paths without it (e.g. foo/bar) can point to either a file or a directory.

In both cases, the timestamp used by Ninja corresponds to the file-system's directory entry itself (which does not change when the files within it are modified).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must say that I fail to see the benefit of this PR. I assumed it is to prepare for glob support but that would require another non-trivial change of logic in Ninja, so why introduce it here and not as a preliminary commit in a PR that does that?


Most filesystems update a directory's modification time whenever entries are
added to or removed from it, so a directory input is a convenient way to
re-run a command when the *set* of files in a directory changes (for example,
to regenerate an index when new source files are added). Changes to the
contents of individual files inside the directory do not normally update the
directory's modification time, so they will not by themselves trigger a rebuild
- list the affected files explicitly (or via a depfile) if that is desired.

A path with a trailing separator that exists but is not a directory is treated
as missing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to correspond to the implementation. If the path points to a file, it will be reported as an error ??


[[validations]]
Validations
~~~~~~~~~~~
Expand Down
5 changes: 4 additions & 1 deletion src/build.cc
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ bool Plan::AddSubTarget(const Node* node, const Node* dependent, string* err,
string referenced;
if (dependent)
referenced = ", needed by '" + dependent->path() + "',";
*err = "'" + node->path() + "'" + referenced +
string display_path = node->path();
if (node->is_directory())
display_path += '/';
*err = "'" + display_path + "'" + referenced +
" missing and no known rule to make it";
}
return false;
Expand Down
14 changes: 13 additions & 1 deletion src/graph.cc
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,19 @@ bool RecomputeOutputsDirtyCache::RecomputeOutputDirty(
} // namespace

bool Node::Stat(DiskInterface* disk_interface, string* err) {
mtime_ = disk_interface->Stat(path_, err);
// For directory inputs, append a trailing slash so that the underlying
// stat() call only succeeds for actual directories. On POSIX, stat()ing a
// regular file with a trailing slash returns ENOTDIR, which the disk
// interface reports as a missing file (mtime == 0). Directories
// successfully stat()ed will report the directory's own mtime, which
// changes whenever entries are added to or removed from the directory.
if (is_directory_) {
string stat_path = path_;
stat_path += '/';
mtime_ = disk_interface->Stat(stat_path, err);
} else {
mtime_ = disk_interface->Stat(path_, err);
}
if (mtime_ == -1) {
return false;
}
Expand Down
11 changes: 11 additions & 0 deletions src/graph.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ struct Node {
generated_by_dep_loader_ = value;
}

/// True if this node refers to a directory (path was given with a trailing
/// slash in the manifest). When stat()ed, the directory's mtime is used to
/// determine whether dependents are out-of-date.
bool is_directory() const { return is_directory_; }
void set_directory(bool value) { is_directory_ = value; }

int id() const { return id_; }
void set_id(int id) { id_ = id; }

Expand Down Expand Up @@ -159,6 +165,11 @@ struct Node {
/// can be loaded before the manifest.
bool generated_by_dep_loader_ = true;

/// True if this node refers to a directory rather than a regular file.
/// Set when a manifest input/output path ends with a trailing slash. When
/// stat()ing, the path is treated as a directory and its mtime is used.
bool is_directory_ = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: there is no need for a boolean flag. Just use path_.back() == '/' here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually scratch that, it depends if you want to treat foo/ and foo in the same manifest as pointing to the same graph Node. From this code it looks like that if there is at least one use of foo/ in the manifest for an input path, then the Node for foo is marked as a directory, even if other uses are not.

This is quite subtle, and also doesn't happen if foo is an output, or comes from a depfile, or a dyndep file.

It would probably make more sense to just check for the trailing separator when adding a new Node instead to cover all cases, otherwise this will lead to very hard-to-debug inconsistencies.


/// A dense integer id for the node, assigned and used by DepsLog.
int id_ = -1;

Expand Down
64 changes: 64 additions & 0 deletions src/graph_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1255,3 +1255,67 @@ TEST_F(GraphTest, PhonyOutputWithValidation) {
ASSERT_EQ(1u, validation_nodes.size());
EXPECT_EQ("valid", validation_nodes[0]->path());
}

// A trailing slash in a manifest input marks the input as a directory. The
// canonical path on the Node has the slash removed, but the node carries the
// is_directory() flag so that Node::Stat() looks up the directory entry.
TEST_F(GraphTest, DirectoryInputParsed) {
ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
"build out: cat indir/\n"));

Node* in_node = GetNode("indir");
ASSERT_TRUE(in_node != NULL);
EXPECT_TRUE(in_node->is_directory());
EXPECT_EQ("indir", in_node->path());
}

// When the directory mtime advances past the output's mtime, the output is
// considered dirty.
TEST_F(GraphTest, DirectoryInputMtimeDirty) {
ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
"build out: cat indir/\n"));
// Initial state: directory and output both at tick 1.
fs_.Create("indir/", "");
fs_.Create("out", "");
fs_.Tick();
// Directory mtime bumped (e.g. a file was added).
fs_.Create("indir/", "");

string err;
EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), NULL, &err));
ASSERT_EQ("", err);
EXPECT_TRUE(GetNode("out")->dirty());
}

// When the directory mtime is unchanged, the output is up-to-date even though
// individual files inside the directory may have changed.
TEST_F(GraphTest, DirectoryInputUpToDate) {
ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
"build out: cat indir/\n"));
fs_.Create("indir/", "");
fs_.Tick();
fs_.Create("out", "");

string err;
EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), NULL, &err));
ASSERT_EQ("", err);
EXPECT_FALSE(GetNode("out")->dirty());
}

// A directory input pointing at a non-existent directory should be treated as
// missing (and therefore dirty), even if a regular file exists at the same
// path without a trailing slash.
TEST_F(GraphTest, DirectoryInputMissingTreatedAsDirty) {
ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
"build out: cat indir/\n"));
// A regular file with the same name as the directory, but no directory
// entry. The virtual filesystem keys entries by exact path, so "indir/"
// is not found.
fs_.Create("indir", "");
fs_.Create("out", "");

string err;
EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), NULL, &err));
ASSERT_EQ("", err);
EXPECT_TRUE(GetNode("out")->dirty());
}
6 changes: 5 additions & 1 deletion src/manifest_parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -363,9 +363,13 @@ bool ManifestParser::ParseEdge(string* err) {
string path = i->Evaluate(env);
if (path.empty())
return lexer_.Error("empty path", err);
// A trailing path separator indicates that the input refers to a directory
// rather than a regular file. CanonicalizePath strips the separator, so
// detect it first.
bool is_directory = path.back() == '/';
uint64_t slash_bits;
CanonicalizePath(&path, &slash_bits);
state_->AddIn(edge, path, slash_bits);
state_->AddIn(edge, path, slash_bits, is_directory);
}
edge->implicit_deps_ = implicit;
edge->order_only_deps_ = order_only;
Expand Down
6 changes: 5 additions & 1 deletion src/state.cc
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,13 @@ Node* State::SpellcheckNode(const string& path) {
return result;
}

void State::AddIn(Edge* edge, StringPiece path, uint64_t slash_bits) {
void State::AddIn(Edge* edge, StringPiece path, uint64_t slash_bits,
bool is_directory) {
Node* node = GetNode(path, slash_bits);
node->set_generated_by_dep_loader(false);
if (is_directory) {
node->set_directory(true);
}
edge->inputs_.push_back(node);
node->AddOutEdge(edge);
}
Expand Down
5 changes: 4 additions & 1 deletion src/state.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ struct State {
/// Add input / output / validation nodes to a given edge. This also
/// ensures that the generated_by_dep_loader() flag for all these nodes
/// is set to false, to indicate that they come from the input manifest.
void AddIn(Edge* edge, StringPiece path, uint64_t slash_bits);
/// If \a is_directory is true, the input refers to a directory (the
/// manifest path ended with a trailing slash).
void AddIn(Edge* edge, StringPiece path, uint64_t slash_bits,
bool is_directory = false);
bool AddOut(Edge* edge, StringPiece path, uint64_t slash_bits, std::string* err);
void AddValidation(Edge* edge, StringPiece path, uint64_t slash_bits);
bool AddDefault(StringPiece path, std::string* error);
Expand Down
Loading