[core] Warn when running a different source tree than the one you are in (#18288)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Jonathan Swoboda
2026-08-11 21:06:57 -04:00
committed by GitHub
co-authored by J. Nick Koston
parent 9f28638ee7
commit be5e28ea9e
2 changed files with 186 additions and 0 deletions
+44
View File
@@ -2509,6 +2509,49 @@ def parse_args(argv):
return parser.parse_args(arguments)
def _warn_if_source_tree_mismatch() -> None:
"""Warn when the checkout the user is standing in is not the one being run.
An editable install records one absolute path, so a venv shared between git
worktrees (or reused after a checkout is copied or renamed) keeps importing
the tree it was installed from. Every command then silently runs, and
compiles, sources the user is not looking at. Only fires inside a checkout,
so ordinary installs never see it.
"""
try:
cwd = Path.cwd()
except OSError:
return # working directory is gone; a diagnostic must not break startup
for candidate in (cwd, *cwd.parents):
if (candidate / "esphome" / "__main__.py").is_file():
standing_in = candidate.resolve()
break
else:
return # not inside a checkout; nothing to compare against
running = Path(__file__).resolve().parent.parent
# Both sides are resolved, so on a case-sensitive filesystem this matches
# plain equality. samefile() compares device and inode, which additionally
# covers a case-insensitive filesystem (macOS) reaching one directory by
# differently cased paths. Falls back to equality if either path is gone.
try:
same = standing_in.samefile(running)
except OSError:
same = standing_in == running
if same:
return
_LOGGER.warning(
"Running ESPHome from a different checkout than the one you are in:\n"
" running from: %s\n"
" you are in: %s\n"
"The installed esphome resolves to the first, so its sources are used.\n"
"Run 'python -m esphome' from the second to use that one instead.",
running,
standing_in,
)
def run_esphome(argv):
from esphome.address_cache import AddressCache
@@ -2527,6 +2570,7 @@ def run_esphome(argv):
args.log_level = "CRITICAL"
setup_log(log_level=args.log_level)
_warn_if_source_tree_mismatch()
if args.command in PRE_CONFIG_ACTIONS:
try:
+142
View File
@@ -18,6 +18,7 @@ import pytest
from pytest import CaptureFixture
from zeroconf import ServiceStateChange
from esphome import __main__ as main
from esphome.__main__ import (
Purpose,
_get_configured_xtal_freq,
@@ -6760,3 +6761,144 @@ def test_check_permissions_unreadable_port() -> None:
pytest.raises(EsphomeError, match="read or write permission"),
):
check_permissions("/dev/ttyUSB99")
def _make_checkout(root: Path) -> Path:
"""Create a directory that looks like an esphome checkout."""
(root / "esphome").mkdir(parents=True)
(root / "esphome" / "__main__.py").write_text("", encoding="utf-8")
return root
def test_warn_source_tree_mismatch_warns_for_other_tree(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Standing in a checkout other than the one being run warns."""
standing_in = _make_checkout(tmp_path / "worktree")
running = _make_checkout(tmp_path / "main")
monkeypatch.chdir(standing_in)
monkeypatch.setattr(main, "__file__", str(running / "esphome" / "__main__.py"))
with caplog.at_level(logging.WARNING):
main._warn_if_source_tree_mismatch()
assert "worktree" in caplog.text
assert "main" in caplog.text
def test_warn_source_tree_mismatch_silent_in_same_tree(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Standing in the tree that is running is the normal case and is silent."""
tree = _make_checkout(tmp_path / "main")
monkeypatch.chdir(tree)
monkeypatch.setattr(main, "__file__", str(tree / "esphome" / "__main__.py"))
with caplog.at_level(logging.WARNING):
main._warn_if_source_tree_mismatch()
assert not caplog.text
def test_warn_source_tree_mismatch_silent_outside_checkout(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""An ordinary install run from a config directory never warns."""
running = _make_checkout(tmp_path / "main")
config_dir = tmp_path / "configs"
config_dir.mkdir()
monkeypatch.chdir(config_dir)
monkeypatch.setattr(main, "__file__", str(running / "esphome" / "__main__.py"))
with caplog.at_level(logging.WARNING):
main._warn_if_source_tree_mismatch()
assert not caplog.text
def test_warn_source_tree_mismatch_silent_in_subdirectory(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A subdirectory of the running tree resolves to that tree, so no warning."""
tree = _make_checkout(tmp_path / "main")
subdir = tree / "esphome" / "components"
subdir.mkdir(parents=True)
monkeypatch.chdir(subdir)
monkeypatch.setattr(main, "__file__", str(tree / "esphome" / "__main__.py"))
with caplog.at_level(logging.WARNING):
main._warn_if_source_tree_mismatch()
assert not caplog.text
def test_warn_source_tree_mismatch_warns_when_stat_fails_on_other_tree(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""The samefile() fallback must still warn when the trees really differ."""
standing_in = _make_checkout(tmp_path / "worktree")
running = _make_checkout(tmp_path / "main")
monkeypatch.chdir(standing_in)
monkeypatch.setattr(main, "__file__", str(running / "esphome" / "__main__.py"))
def raise_oserror(self: Path, other: Path) -> bool:
raise OSError("stat failed")
monkeypatch.setattr(Path, "samefile", raise_oserror)
with caplog.at_level(logging.WARNING):
main._warn_if_source_tree_mismatch()
assert "worktree" in caplog.text
def test_warn_source_tree_mismatch_silent_when_cwd_is_gone(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A deleted working directory must not turn the diagnostic into a traceback."""
running = _make_checkout(tmp_path / "main")
monkeypatch.setattr(main, "__file__", str(running / "esphome" / "__main__.py"))
def raise_filenotfound() -> Path:
raise FileNotFoundError("cwd is gone")
monkeypatch.setattr(Path, "cwd", staticmethod(raise_filenotfound))
with caplog.at_level(logging.WARNING):
main._warn_if_source_tree_mismatch()
assert not caplog.text
def test_warn_source_tree_mismatch_falls_back_when_stat_fails(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""If samefile() cannot stat, fall back to comparing the paths."""
tree = _make_checkout(tmp_path / "main")
monkeypatch.chdir(tree)
monkeypatch.setattr(main, "__file__", str(tree / "esphome" / "__main__.py"))
def raise_oserror(self: Path, other: Path) -> bool:
raise OSError("stat failed")
monkeypatch.setattr(Path, "samefile", raise_oserror)
with caplog.at_level(logging.WARNING):
main._warn_if_source_tree_mismatch()
# Same tree, so the path comparison still finds them equal and stays silent
assert not caplog.text