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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ Bazel reads each watched file in full when a new server first uses the
repository. Leaving the system volume out spares that check a read of every
system tool, which on a freshly booted host can take tens of seconds.

Bazel records a watched link to a directory only as a directory, not where it
points, so the rule watches through such links instead: a search path directory
named by a link, and the `xcode-select` selection, through its `xcodebuild`.
Pointing one at different tools recomputes the key. Pointing one at a copy with
the same names and bytes does not, and the key keeps naming the old path until
something else recomputes it; the tools that run are the same.

## Comparing two machines

The key and every field it digests are in `key.json`:
Expand Down
14 changes: 11 additions & 3 deletions host_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,16 +173,20 @@ def search_path_identity(
digest.update(f"{directory}={resolved}\n".encode())
if directory != resolved:
watches.file(directory)
if not on_system_volume(resolved, system_volume):
watches.listing(resolved)
# Watches go through the directory as named. Bazel records a watched
# link to a directory only as a directory, so a watch on the resolved
# path would never see the link moved; through the link, it reaches
# whatever the link points at now.
if not on_system_volume(directory, system_volume):
watches.listing(directory)
if resolved in listed:
continue
listed.add(resolved)
if not resolved.is_dir():
digest.update(b"absent\n")
continue
# PATH search looks at direct entries only, never into subdirectories.
for entry in sorted(resolved.iterdir()):
for entry in sorted(directory.iterdir()):
identity = entry_identity(entry, digests, watches, system_volume)
digest.update(f"{identity}\n".encode())
return digest.hexdigest()
Expand Down Expand Up @@ -212,6 +216,10 @@ def developer_tools_version(developer_dir: Path, watches: Watches) -> str:
def macos_fields(watches: Watches) -> dict[str, object]:
watches.file(MACOS_VERSION)
watches.file(XCODE_SELECTION)
# The selection is a link to a directory, which Bazel records only as a
# directory. Moving it is seen through this file instead: its bytes differ
# from one Xcode build to the next, and the Command Line Tools have none.
watches.file(XCODE_SELECTION / "usr" / "bin" / "xcodebuild")
try:
developer_dir = command_output("/usr/bin/xcode-select", "--print-path")
except subprocess.CalledProcessError:
Expand Down
16 changes: 15 additions & 1 deletion tests/check_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,10 @@ def expect(step: str, stderr: str, wanted: str) -> None:


def check_watched_changes(bazel: str, module_root: Path, scratch: Path) -> None:
# The search path names the tools through a link to their directory.
tools = scratch / "tools"
tools.mkdir()
(scratch / "tools-one").mkdir()
tools.symlink_to("tools-one")
tool = tools / "tool"
tool.write_bytes(b"one")
prefix = scratch / "brew"
Expand Down Expand Up @@ -195,6 +197,18 @@ def check_watched_changes(bazel: str, module_root: Path, scratch: Path) -> None:
tool.write_bytes(b"one")
write_sbom(keg, BOTTLE)
expect("original tools restored", consumer.build(second), "disk cache hit")

# Bazel records a watched link to a directory only as a directory, so
# only a watch through the link sees it pointed at other tools.
(scratch / "tools-two").mkdir()
(scratch / "tools-two" / "tool").write_bytes(b"other")
tools.unlink()
tools.symlink_to("tools-two")
expect(
"tool directory link moved, running server",
consumer.build(second),
"executed",
)
finally:
consumer.shutdown()

Expand Down
24 changes: 24 additions & 0 deletions tests/host_key_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,18 @@ def test_retargeted_link_rekeys(self) -> None:
link.symlink_to("second")
self.assertNotEqual(before, self.identity())

def test_linked_directory_is_watched_through_its_link(self) -> None:
# Bazel records a watched link to a directory only as a directory, so
# watches under the resolved path would never see the link moved.
(self.directory / "one").mkdir()
(self.directory / "one" / "tool").write_bytes(b"one")
linked = self.directory / "tools"
linked.symlink_to("one")
watches = host_key.Watches()
host_key.search_path_identity([linked], watches, SYSTEM_VOLUME)
self.assertIn(str(linked / "tool"), watches.files)
self.assertIn(str(linked), watches.directories)

@unittest.skipIf(os.geteuid() == 0, "root reads every file")
def test_unreadable_file_ignores_timestamps(self) -> None:
# An inode or a timestamp would differ between hosts holding the same
Expand Down Expand Up @@ -235,5 +247,17 @@ def test_path_through_a_movable_directory_link_is_not_covered(self) -> None:
self.assertFalse(covered(self.directory / "bin" / "sh", SYSTEM_VOLUME))


@unittest.skipUnless(sys.platform == "darwin", "only macOS selects an Xcode")
class XcodeSelectionTest(unittest.TestCase):
def test_selection_is_watched_through_its_link(self) -> None:
# xcode-select moves a link to a directory, which Bazel records only as
# a directory; without a file watched through it, a newly selected
# Xcode would keep the old one's key.
watches = host_key.Watches()
host_key.macos_fields(watches)
through = host_key.XCODE_SELECTION / "usr" / "bin" / "xcodebuild"
self.assertIn(str(through), watches.files)


if __name__ == "__main__":
unittest.main()
Loading