Fall back to $builddir/target when target lookup fails

When $builddir is set in the manifest, passing "foo" on the command
line now checks for a target named "foo" first, then falls back to
"$builddir/foo" before reporting an error.
This commit is contained in:
Jan Niklas Hasse
2026-04-12 15:25:27 +02:00
parent 947aade849
commit 93e431abd3
2 changed files with 129 additions and 0 deletions

View File

@@ -331,6 +331,16 @@ Node* NinjaMain::CollectTarget(const char* cpath, string* err) {
}
Node* node = state_.LookupNode(path);
if (!node && !build_dir_.empty()) {
string builddir_path = build_dir_ + "/" + path;
uint64_t builddir_slash_bits;
CanonicalizePath(&builddir_path, &builddir_slash_bits);
node = state_.LookupNode(builddir_path);
if (node) {
path = builddir_path;
slash_bits = builddir_slash_bits;
}
}
if (node) {
if (first_dependent) {
if (node->out_edges().empty()) {

View File

@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""Integration test for target lookup with builddir fallback."""
import os
import shutil
import subprocess
import tempfile
import unittest
NINJA_PATH = os.path.abspath("./ninja")
class BuilddirTargetTest(unittest.TestCase):
"""Test that targets can be found via $builddir fallback."""
def setUp(self):
self.test_dir = tempfile.mkdtemp(prefix="ninja_builddir_target_test_")
self.original_dir = os.getcwd()
os.chdir(self.test_dir)
def tearDown(self):
os.chdir(self.original_dir)
shutil.rmtree(self.test_dir, ignore_errors=True)
def _run_ninja(self, *args):
result = subprocess.run(
[NINJA_PATH] + list(args), capture_output=True, text=True
)
return result
def test_builddir_fallback(self):
"""Passing 'foo' should build '$builddir/foo' when no literal 'foo' target exists."""
with open("build.ninja", "w") as f:
f.write(
"builddir = out\n"
"\n"
"rule touch\n"
" command = touch $out\n"
"\n"
"build $builddir/foo: touch\n"
)
result = self._run_ninja("foo")
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
self.assertTrue(os.path.exists("out/foo"), "out/foo was not created")
def test_exact_match_takes_priority(self):
"""A literal target 'foo' should be preferred over '$builddir/foo'."""
with open("build.ninja", "w") as f:
f.write(
"builddir = out\n"
"\n"
"rule cp\n"
" command = cp $in $out\n"
"\n"
"rule touch\n"
" command = touch $out\n"
"\n"
"build foo: touch\n"
"build $builddir/foo: cp foo\n"
)
result = self._run_ninja("foo")
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
# Only the exact-match target should have been built; out/foo depends on
# foo so if ninja built out/foo both would exist, but requesting just
# "foo" should only build the literal target.
self.assertTrue(os.path.exists("foo"), "foo was not created")
self.assertFalse(
os.path.exists("out/foo"),
"out/foo should not have been built when exact 'foo' exists",
)
def test_no_builddir_no_fallback(self):
"""Without builddir, an unknown target should still fail."""
with open("build.ninja", "w") as f:
f.write(
"rule touch\n"
" command = touch $out\n"
"\n"
"build bar: touch\n"
)
result = self._run_ninja("nonexistent")
self.assertNotEqual(result.returncode, 0)
self.assertIn("unknown target", result.stderr)
def test_fallback_with_subdirectory(self):
"""Fallback should work for paths with subdirectories."""
with open("build.ninja", "w") as f:
f.write(
"builddir = out\n"
"\n"
"rule touch\n"
" command = mkdir -p `dirname $out` && touch $out\n"
"\n"
"build $builddir/sub/bar: touch\n"
)
result = self._run_ninja("sub/bar")
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
self.assertTrue(os.path.exists("out/sub/bar"), "out/sub/bar was not created")
def test_fallback_miss_still_errors(self):
"""When the target isn't found even under $builddir, an error is reported."""
with open("build.ninja", "w") as f:
f.write(
"builddir = out\n"
"\n"
"rule touch\n"
" command = touch $out\n"
"\n"
"build $builddir/foo: touch\n"
)
result = self._run_ninja("nonexistent")
self.assertNotEqual(result.returncode, 0)
self.assertIn("unknown target", result.stderr)