Bug 1759084: Print reason for site being out-of-date r=ahal

There are cases in CI where sites are being considered out-of-date,
despite the situation being unexpected - for example, the
"site out-of-date even though already activated" issue described in
this patch's associated bug.

Return a `reason` string from `_is_venv_up_to_date()`, and print it in
cases where a virtualenv being out-of-date is erroneous.

Differential Revision: https://phabricator.services.mozilla.com/D140855
This commit is contained in:
Mitchell Hentges 2022-03-21 21:08:23 +00:00
parent 25ca8bd87b
commit a822813370

View File

@ -49,6 +49,12 @@ class InstallPipRequirementsException(Exception):
pass pass
class SiteUpToDateResult:
def __init__(self, is_up_to_date, reason=None):
self.is_up_to_date = is_up_to_date
self.reason = reason
class SitePackagesSource(enum.Enum): class SitePackagesSource(enum.Enum):
NONE = "none" NONE = "none"
SYSTEM = "system" SYSTEM = "system"
@ -338,7 +344,7 @@ class MachSiteManager:
def _up_to_date(self): def _up_to_date(self):
if self._site_packages_source == SitePackagesSource.NONE: if self._site_packages_source == SitePackagesSource.NONE:
return True return SiteUpToDateResult(True)
elif self._site_packages_source == SitePackagesSource.SYSTEM: elif self._site_packages_source == SitePackagesSource.SYSTEM:
pthfile_lines = [ pthfile_lines = [
*self._requirements.pths_as_absolute(self._topsrcdir), *self._requirements.pths_as_absolute(self._topsrcdir),
@ -347,7 +353,7 @@ class MachSiteManager:
_assert_pip_check( _assert_pip_check(
self._topsrcdir, pthfile_lines, "mach", self._requirements self._topsrcdir, pthfile_lines, "mach", self._requirements
) )
return True return SiteUpToDateResult(True)
elif self._site_packages_source == SitePackagesSource.VENV: elif self._site_packages_source == SitePackagesSource.VENV:
environment = self._virtualenv() environment = self._virtualenv()
return _is_venv_up_to_date( return _is_venv_up_to_date(
@ -359,15 +365,15 @@ class MachSiteManager:
) )
def ensure(self, *, force=False): def ensure(self, *, force=False):
up_to_date = self._up_to_date() result = self._up_to_date()
if force or not up_to_date: if force or not result.is_up_to_date:
if Path(sys.prefix) == Path(self._metadata.prefix): if Path(sys.prefix) == Path(self._metadata.prefix):
# If the Mach virtualenv is already activated, then the changes caused # If the Mach virtualenv is already activated, then the changes caused
# by rebuilding the virtualenv won't take effect until the next time # by rebuilding the virtualenv won't take effect until the next time
# Mach is used, which can lead to confusing one-off errors. # Mach is used, which can lead to confusing one-off errors.
# Instead, request that the user resolve the out-of-date situation, # Instead, request that the user resolve the out-of-date situation,
# *then* come back and run the intended command. # *then* come back and run the intended command.
raise VirtualenvOutOfDateException() raise VirtualenvOutOfDateException(result.reason)
self._build() self._build()
def attempt_populate_optional_packages(self): def attempt_populate_optional_packages(self):
@ -560,9 +566,11 @@ class CommandSiteManager:
If using a virtualenv Python binary directly, it's useful to call this function If using a virtualenv Python binary directly, it's useful to call this function
first to ensure that the virtualenv doesn't have obsolete references or packages. first to ensure that the virtualenv doesn't have obsolete references or packages.
""" """
if not self._up_to_date(): result = self._up_to_date()
if not result.is_up_to_date:
active_site = MozSiteMetadata.from_runtime() active_site = MozSiteMetadata.from_runtime()
if active_site.site_name == self._site_name: if active_site.site_name == self._site_name:
print(result.reason, file=sys.stderr)
raise Exception( raise Exception(
f'The "{self._site_name}" site is out-of-date, even though it has ' f'The "{self._site_name}" site is out-of-date, even though it has '
f"already been activated. Was it modified while this Mach process " f"already been activated. Was it modified while this Mach process "
@ -1201,8 +1209,12 @@ def _is_venv_up_to_date(
expected_metadata, expected_metadata,
): ):
if not os.path.exists(target_venv.prefix): if not os.path.exists(target_venv.prefix):
return False return SiteUpToDateResult(False, f'"{target_venv.prefix}" does not exist')
# Modifications to any of the following files mean the virtualenv should be
# rebuilt:
# * The `virtualenv` package
# * Any of our requirements manifest files
virtualenv_package = os.path.join( virtualenv_package = os.path.join(
topsrcdir, topsrcdir,
"third_party", "third_party",
@ -1212,41 +1224,50 @@ def _is_venv_up_to_date(
"version.py", "version.py",
) )
deps = [virtualenv_package] + requirements.requirements_paths deps = [virtualenv_package] + requirements.requirements_paths
# Modifications to any of the following files mean the virtualenv should be
# rebuilt:
# * This file
# * The `virtualenv` package
# * Any of our requirements manifest files
metadata_mtime = os.path.getmtime( metadata_mtime = os.path.getmtime(
os.path.join(target_venv.prefix, METADATA_FILENAME) os.path.join(target_venv.prefix, METADATA_FILENAME)
) )
dep_mtime = max(os.path.getmtime(p) for p in deps) for dep_file in deps:
if dep_mtime > metadata_mtime: if os.path.getmtime(dep_file) > metadata_mtime:
return False return SiteUpToDateResult(
False, f'"{dep_file}" has changed since the virtualenv was created'
)
try: try:
existing_metadata = MozSiteMetadata.from_path(target_venv.prefix) existing_metadata = MozSiteMetadata.from_path(target_venv.prefix)
except MozSiteMetadataOutOfDateError: except MozSiteMetadataOutOfDateError as e:
# The metadata is missing required fields, so must be out-of-date. # The metadata is missing required fields, so must be out-of-date.
return False return SiteUpToDateResult(False, str(e))
if existing_metadata != expected_metadata: if existing_metadata != expected_metadata:
# The metadata doesn't exist or some fields have different values. # The metadata doesn't exist or some fields have different values.
return False return SiteUpToDateResult(
False,
f"The existing metadata on-disk ({vars(existing_metadata)}) does not match "
f"the expected metadata ({vars(expected_metadata)}",
)
platlib_site_packages_dir = target_venv.resolve_sysconfig_packages_path("platlib") platlib_site_packages_dir = target_venv.resolve_sysconfig_packages_path("platlib")
pthfile_path = os.path.join(platlib_site_packages_dir, PTH_FILENAME)
try: try:
with open(os.path.join(platlib_site_packages_dir, PTH_FILENAME)) as file: with open(pthfile_path) as file:
current_pthfile_contents = file.read().strip() current_pthfile_contents = file.read().strip()
except FileNotFoundError: except FileNotFoundError:
return False return SiteUpToDateResult(False, f'No pthfile found at "{pthfile_path}"')
expected_pthfile_contents = "\n".join(expected_pthfile_lines) expected_pthfile_contents = "\n".join(expected_pthfile_lines)
if current_pthfile_contents != expected_pthfile_contents: if current_pthfile_contents != expected_pthfile_contents:
return False return SiteUpToDateResult(
False,
f'The pthfile at "{pthfile_path}" does not match the expected value.\n'
f"# --- on-disk pthfile: ---\n"
f"{current_pthfile_contents}\n"
f"# --- expected pthfile contents ---\n"
f"{expected_pthfile_contents}\n"
f"# ---",
)
return True return SiteUpToDateResult(True)
def activate_virtualenv(virtualenv: PythonVirtualenv): def activate_virtualenv(virtualenv: PythonVirtualenv):