uberpoet/statemanagement.py (133 lines of code) (raw):

# Copyright (c) 2018 Uber Technologies, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import import getpass import logging import os import shutil import subprocess import sys from os.path import join from .util import pad_list class SettingsState(object): def __init__(self, git_root): self.git_root = git_root self.have_backed_up = False self.local_path = join(self.git_root, '.buckconfig.local') self.backup_path = join(self.git_root, '.buckconfig.local.bak') self.select_path = None def save_buckconfig_local(self): if os.path.exists(self.backup_path): os.remove(self.backup_path) if os.path.exists(self.local_path): logging.info('Backing up .buckconfig.local to .buckconfig.local.bak') shutil.copy2(self.local_path, self.backup_path) self.have_backed_up = True else: logging.info('No .buckconfig.local to back up, skipping') def restore_buckconfig_local(self): if self.have_backed_up: logging.info('Restoring .buckconfig.local') os.remove(self.local_path) shutil.copy2(self.backup_path, self.local_path) os.remove(self.backup_path) self.have_backed_up = False def save_xcode_select(self): self.select_path = subprocess.check_output(['xcode-select', '-p']).rstrip() def restore_xcode_select(self): if self.select_path: logging.info('Restoring xcode-select path to %s', self.select_path) subprocess.check_call(['sudo', 'xcode-select', '-s', self.select_path]) class XcodeManager(object): @staticmethod def get_xcode_dirs(containing_dir='/Applications'): items = os.listdir(containing_dir) return [join(containing_dir, d) for d in items if 'xcode' in d.lower() and d.endswith('app')] @staticmethod def get_current_xcode_version(): version_out = subprocess.check_output(['xcodebuild', '-version']).splitlines() version_num = version_out[0].split(' ')[1] build_id = version_out[1].split(' ')[2] return version_num, build_id @staticmethod def switch_xcode_version(xcode_path): subprocess.check_call(['sudo', 'xcode-select', '-s', xcode_path]) def xcode_version_of_path(self, path): try: self.switch_xcode_version(path) except subprocess.CalledProcessError: return None, None return self.get_current_xcode_version() def discover_xcode_versions(self): settings = SettingsState('/') settings.save_xcode_select() candidates = self.get_xcode_dirs() out = {} for path in candidates: version, build = self.xcode_version_of_path(path) if version: out[(version, build)] = path settings.restore_xcode_select() out = XcodeVersion.choose_latest_major_versions(out) return out @staticmethod def _get_global_module_cache_dir(): try: username = getpass.getuser() except Exception as e: sys.exit(str(e)) cache_dir = subprocess.check_output(['getconf', 'DARWIN_USER_CACHE_DIR']).rstrip() user_dir = 'org.llvm.clang.{}'.format(username) return os.path.join(cache_dir, user_dir, 'ModuleCache') def clean_caches(self): logging.info('Cleaning Xcode caches...') directories_to_delete = ( '~/Library/Caches/com.apple.dt.Xcode', '~/Library/Developer/Xcode/DerivedData', self._get_global_module_cache_dir(), ) for directory in directories_to_delete: full_path = os.path.expanduser(directory) logging.info('Removing %s', full_path) subprocess.check_call(['rm', '-fr', full_path]) class XcodeVersion(object): """Represents an xcode version that is comparable""" def __init__(self, raw_version, build): self.version = self.numeric_version(raw_version) self.raw_version = raw_version self.build = build @staticmethod def numeric_version(raw): return pad_list([int(x) for x in raw.split('.')], 3, 0) @property def major(self): return self.version[0] @property def raw(self): return self.raw_version, self.build @staticmethod def choose_latest_major_versions(raw_versions): """ This selects the latest version for each major version of xcode in a set of xcode paths. raw_versions is a {(version_str, build_str): xcode_path_str} dictionary. """ versions = {XcodeVersion(raw_version, build): path for (raw_version, build), path in raw_versions.iteritems()} major_seperated = {} for version, path in versions.iteritems(): subset = major_seperated.get(version.major, {}) subset[version] = path major_seperated[version.major] = subset out = {} for subset in major_seperated.values(): max_version = max(subset.keys()) out[max_version.raw] = subset[max_version] return out def __repr__(self): return "XcodeVersion('{}','{}')".format(self.raw_version, self.build) def __eq__(self, b): if not (tuple(self.version) == tuple(b.version)): return False if self.build == b.build: return True return False def __gt__(self, b): if self.version == b.version: if self.build > b.build: return True return False if tuple(self.version) > tuple(b.version): return True return False