mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[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:
co-authored by
J. Nick Koston
parent
9f28638ee7
commit
be5e28ea9e
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user