tools/upgrade/commands/fix_configuration.py (111 lines of code) (raw):
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
import argparse
import logging
import subprocess
from pathlib import Path
from typing import Optional
from .. import UserError
from ..configuration import Configuration
from ..filesystem import path_exists
from ..repository import Repository
from .command import CommandArguments, ErrorSuppressingCommand
from .consolidate_nested_configurations import consolidate_nested
LOG: logging.Logger = logging.getLogger(__name__)
class FixConfiguration(ErrorSuppressingCommand):
def __init__(
self, command_arguments: CommandArguments, *, repository: Repository, path: Path
) -> None:
super().__init__(command_arguments, repository)
self._path: Path = path
self._configuration: Optional[Configuration] = Configuration(
path / ".pyre_configuration.local"
)
@staticmethod
def from_arguments(
arguments: argparse.Namespace, repository: Repository
) -> "FixConfiguration":
command_arguments = CommandArguments.from_arguments(arguments)
return FixConfiguration(
command_arguments, repository=repository, path=arguments.path
)
@classmethod
# pyre-fixme[40]: Non-static method `add_arguments` cannot override a static
# method defined in `ErrorSuppressingCommand`.
def add_arguments(cls, parser: argparse.ArgumentParser) -> None:
super(FixConfiguration, cls).add_arguments(parser)
parser.set_defaults(command=cls.from_arguments)
parser.add_argument(
"path",
help="Path to project root with local configuration",
type=path_exists,
)
def _remove_bad_targets(self) -> None:
configuration = self._configuration
if not configuration:
return
targets = configuration.targets
if not targets:
return
buildable_targets = []
for target in targets:
build_command = ["buck", "query", target]
try:
subprocess.check_output(build_command, timeout=30)
except subprocess.TimeoutExpired:
buildable_targets.append(target)
except subprocess.CalledProcessError:
LOG.info(f"Removing bad target: {target}")
pass
else:
buildable_targets.append(target)
if len(buildable_targets) == 0 and not configuration.source_directories:
LOG.info(f"Removing empty configuration at: {configuration.get_path()}")
self._repository.remove_paths([configuration.get_path()])
self._configuration = None
else:
configuration.targets = buildable_targets
configuration.write()
def _consolidate_nested(self) -> None:
parent_local_configuration_path = Configuration.find_parent_file(
".pyre_configuration.local", self._path.parent
)
if not parent_local_configuration_path:
return
parent_local_configuration = Configuration(parent_local_configuration_path)
ignored_subdirectories = parent_local_configuration.ignore_all_errors or []
if (
str(self._path.relative_to(parent_local_configuration_path.parent))
in ignored_subdirectories
):
return
LOG.info(
f"Consolidating with configuration at: {parent_local_configuration_path}"
)
consolidate_nested(
self._repository,
parent_local_configuration_path,
[self._path / ".pyre_configuration.local"],
)
self._configuration = parent_local_configuration
def _commit_changes(self) -> None:
title = "Fix broken configuration for {}".format(str(self._path))
self._repository.commit_changes(
commit=(not self._no_commit),
title=title,
summary="Cleaning up broken pyre configurations by removing targets "
+ "that cannot build and removing nested configurations where applicable.",
reviewers=["pyre", "sentinel"],
)
def run(self) -> None:
self._remove_bad_targets()
self._consolidate_nested()
# Clean any revealed errors.
configuration = self._configuration
if configuration:
try:
self._get_and_suppress_errors(configuration)
except UserError as error:
LOG.warning(
f"Configuration at {configuration.get_path()} still "
+ f"does not build:\n{str(error)}."
)
LOG.warning("Discarding changes.")
self._repository.revert_all(remove_untracked=False)
return
self._commit_changes()