Last active
October 7, 2018 23:33
-
-
Save cjlawson02/9a628d000c83b5763667fef157402e7c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
''' | |
Greybots Environment Installer | |
''' | |
from argparse import ArgumentParser | |
from inspect import getdoc | |
from os import chdir, path as ospath | |
from pathlib import Path | |
from shutil import rmtree | |
from subprocess import Popen, CalledProcessError, PIPE, STDOUT | |
import sys | |
from platform import system | |
from logging import basicConfig, getLogger, INFO, DEBUG, WARNING | |
LOGGER = getLogger('greybots.installer') | |
#################### | |
# Misc Functions # | |
#################### | |
# User home directory | |
def user_home(): | |
''' | |
User home directory returner. | |
Some python versions do not support Path.home() | |
''' | |
try: | |
return Path.home() | |
except AttributeError: | |
return ospath.expanduser('~') | |
# Yes no prompt | |
def query_yes_no(question, default='yes'): | |
'''Ask a yes/no question via input() and return their answer. | |
'question' is a string that is presented to the user. | |
'default' is the presumed answer if the user just hits <Enter>. | |
It must be 'yes' (the default), 'no' or None (meaning | |
an answer is required of the user). | |
The 'answer' return value is True for 'yes' or False for 'no'. | |
''' | |
valid = {'yes': True, 'y': True, 'ye': True, | |
'no': False, 'n': False} | |
if default is None: | |
prompt = ' [y/n] ' | |
elif default == 'yes': | |
prompt = ' [Y/n] ' | |
elif default == 'no': | |
prompt = ' [y/N] ' | |
else: | |
raise GeneralError('Invalid default answer: {}'.format(default)) | |
while True: | |
LOGGER.warning(question + prompt) | |
try: | |
choice = input().lower() | |
if default is not None and choice == '': | |
return valid[default] | |
elif choice in valid: | |
return valid[choice] | |
else: | |
LOGGER.error("Invalid option. Please respond with 'yes' or " | |
" 'no' (or 'y' or 'n')") | |
except ValueError: | |
LOGGER.error("An error occured. Try again.") | |
################### | |
# Error Classes # | |
################### | |
# A general error, typically used for unexpected failures. | |
class GeneralError(Exception): | |
''' | |
General error handler. | |
''' | |
def __init__(self, message, *args): | |
super(GeneralError, self).__init__(message) | |
self.message = message | |
LOGGER.exception('GeneralError: %s', self.message, *args) | |
exit(1) | |
# General shell command error handler. | |
class ShellError(Exception): | |
''' | |
A shell error. Used when a shell command doesn't exist or exits non-zero. | |
''' | |
def __init__(self, message, *args): | |
super(ShellError, self).__init__(message) | |
self.message = message | |
LOGGER.exception('ShellError: %s', self.message, *args) | |
exit(2) | |
#################### | |
# Shell Commands # | |
#################### | |
# Run a shell command. | |
def run(command): | |
''' | |
A subprocess.Popen wrapper with logging. | |
''' | |
LOGGER.debug('=> %s', command) | |
try: | |
process = Popen(command, stdout=PIPE, stderr=STDOUT) | |
exitcode = process.wait() | |
with process.stdout: | |
for stdout in iter(process.stdout.readline, b''): | |
LOGGER.debug('%s: %r', command, stdout) | |
if exitcode != 0: | |
raise CalledProcessError(exitcode, command) | |
except CalledProcessError as exception: | |
raise ShellError('{} Failed: {}'.format(command, exception)) | |
else: | |
LOGGER.debug('%s finished successfully', command) | |
# Search if a particular shell application is installed. | |
def find_application(name): | |
''' | |
Application finder, used to check for preinstalled dependencies. | |
''' | |
LOGGER.debug('Searching for %s...', name) | |
try: | |
run(name) | |
except OSError: | |
LOGGER.debug('Could not find %s', name) | |
return False | |
else: | |
LOGGER.debug('Found %s', name) | |
return True | |
# Search if java is installed on the mac platform. | |
# TODO: find the actual exception type | |
def find_mac_jdk8(): | |
''' | |
Special finder for JDK 8 on Mac... Mac has a fake library built in. | |
''' | |
LOGGER.debug('Searching for macOS JDK 8...') | |
try: | |
run([Path('/usr/libexec/java_home'), '-v', '1.8']) | |
except: | |
LOGGER.debug('Could not find macOS JDK 8') | |
return False | |
else: | |
LOGGER.debug('Found macOS JDK 8') | |
return True | |
# Apt package installer. | |
def apt_install(packages): | |
''' | |
Package installer for Debian-based ems with apt. | |
''' | |
LOGGER.debug('Installing %s using apt', packages) | |
run(['sudo', 'apt', 'install', '-y', packages]) | |
# Apt source updater. | |
def apt_update(): | |
''' | |
Update apt's source list. | |
''' | |
LOGGER.debug('Updating apt') | |
run(['sudo', 'apt', 'update']) | |
# Apt upgrader. | |
def apt_upgrade(): | |
''' | |
Upgrade apt packages. | |
''' | |
LOGGER.debug('Upgrading apt packages') | |
run(['sudo', 'apt', 'upgrade', '-y']) | |
def get_distro_version(): | |
''' | |
Version of your Linux distro. | |
''' | |
if system() == 'Linux': | |
with open('/etc/issue') as version: | |
return version.read().lower().split()[1] | |
######################### | |
# Installer Functions # | |
######################### | |
# Check command optional arguments. | |
def check_opts(check_parser): | |
''' | |
Optional arguments for the check option. | |
''' | |
# Run a dependency check. | |
def check(options): | |
''' | |
Check if dependencies and extras are installed. | |
''' | |
if system() != 'Windows': | |
LOGGER.info('Running dependency checker...') | |
LOGGER.debug('Arguments: %s', options) | |
LOGGER.info('Git: %s', find_application(['git', '--version'])) | |
LOGGER.info('cURL: %s', find_application(['curl', '-V'])) | |
if system() == 'Darwin': | |
LOGGER.info('Homebrew: %s', find_application(['brew', '-v'])) | |
LOGGER.info('JDK 8: %s', find_mac_jdk8()) | |
else: | |
LOGGER.info('Apt: %s', find_application(['apt', '-v'])) | |
LOGGER.info('software-properties-common: %s', find_application([ | |
'add-apt-repository', '-h'])) | |
LOGGER.info('JDK 8: %s', find_application(['java', '-version'])) | |
LOGGER.info('Clang: %s', find_application(['clang', '-v'])) | |
LOGGER.info('Clang Format: %s', find_application(['clang-format', | |
'-version'])) | |
LOGGER.info('frc-toolchain: %s', | |
find_application(['arm-frc-linux-gnueabi-g++', | |
'--version'])) | |
LOGGER.info('Bazel: %s', find_application(['bazel', 'version'])) | |
else: | |
raise GeneralError('Platform is not supported.') | |
# Install command optional arguments. | |
def install_opts(install_parser): | |
''' | |
Optional arguments for the install option. | |
''' | |
install_parser.add_argument('--build', | |
help='automatically compile code to test ' | |
'Bazel', | |
action='store_true') | |
install_parser.add_argument('--clone-repo', | |
help='automatically clone the repo', | |
action='store_true') | |
install_parser.add_argument('--github', | |
help='clone/build repositories from a ' | |
'specific directory other than ' | |
'~/Documents/GitHub') | |
install_parser.add_argument('--repo', | |
help='specify a repo to use for clone/build ' | |
'from Team973') | |
install_parser.add_argument('--travis', | |
help='Travis CI mode', | |
action='store_true') | |
# Install and configure the environment. | |
def install(options): | |
''' | |
Main installer for environment. | |
''' | |
if system() != 'Windows': | |
cache_root = Path(user_home(), 'greybots_installer_cache') | |
run(['mkdir', '-p', cache_root]) | |
LOGGER.debug('Caching files at %s', cache_root) | |
LOGGER.info('Running installer...') | |
LOGGER.debug('Arguments: %s', options) | |
LOGGER.debug('Cache root: %s', cache_root) | |
if system() == 'Darwin': | |
###################### | |
# Package Managers # | |
###################### | |
# Homebrew should already be installed to run python3, but in some | |
# rare case, check for it. | |
if not find_application(['brew', '-v']): | |
raise GeneralError('Please install Homebrew before running.') | |
############################# | |
# Development Dependencies # | |
############################# | |
# JDK 8 | |
if not find_mac_jdk8(): | |
LOGGER.info('Installing Java Development Kit...') | |
run(['brew', 'tap', | |
'caskroom/versions']) | |
run(['brew', 'cask', | |
'install', 'java8']) | |
# clang, macOS has it in xcode-install | |
if not find_application(['clang-format', '-version']): | |
LOGGER.info('Installing Clang Format...') | |
run(['brew', 'install', | |
'clang-format']) | |
# frc-toolchain | |
if not find_application(['arm-frc-linux-gnueabi-g++', | |
'--version']): | |
LOGGER.info('Installing frc-toolchain...') | |
frc_tar = Path(cache_root, 'FRC-Toolchain.pkg.tar.gz') | |
run(['curl', '-s', '-o', frc_tar, | |
'http://first.wpi.edu/FRC/roborio/toolchains/' | |
'FRC-2018-OSX-Toolchain-5.5.pkg.tar.gz']) | |
run(['tar', 'xvf', frc_tar, '-C', cache_root]) | |
run(['sudo', 'installer', '-pkg', | |
Path(cache_root, 'FRC ARM Toolchain.pkg/'), '-target', | |
'/']) | |
# Bazel | |
if not find_application(['bazel', 'version']): | |
LOGGER.info('Installing Bazel...') | |
run(['brew', 'install', | |
'bazel']) | |
elif system() == 'Linux': | |
###################### | |
# Package Managers # | |
###################### | |
# Apt | |
if not find_application(['apt', '-v']): | |
raise GeneralError('Apt not installed. Probably not a ' | |
'compatible Debian platform.') | |
elif find_application(['apt', '-v']): | |
LOGGER.info('Updating and upgrading APT...') | |
apt_update() | |
apt_upgrade() | |
# software-properties-common | |
if not find_application(['add-apt-repository', '-h']): | |
LOGGER.info('Installing software-properties-common...') | |
apt_install('software-properties-common') | |
# git, required and repositories | |
if not find_application(['git', '--version']): | |
LOGGER.info('Installing Git...') | |
apt_install('git') | |
if not find_application(['curl', '-V']): | |
LOGGER.info('Installing cURL...') | |
apt_install('curl') | |
############################# | |
# Development Dependencies # | |
############################# | |
# JDK 8 | |
if not find_application(['java', '-version']): | |
LOGGER.info('Installing Java Development Kit...') | |
apt_install('openjdk-8-jdk') | |
# clang | |
if not find_application(['clang', '-v']): | |
LOGGER.info('Installing Clang...') | |
apt_install('clang') | |
if not (find_application(['clang-format', '-version']) or options.travis): | |
LOGGER.info('Installing Clang Format...') | |
apt_install('clang-format') | |
# frc-toolchain | |
if not find_application(['arm-frc-linux-gnueabi-g++', | |
'--version']): | |
LOGGER.info('Installing frc-toolchain...') | |
# FIXME: Temporary fix for WPILIB's PPA. | |
if get_distro_version() == '18.04' or get_distro_version() == '18.04.1': | |
wpilib_apt = open(Path(cache_root, 'wpilib.list'), 'w') | |
wpilib_apt.write('deb http://ppa.launchpad.net/wpilib/toolchain/ubuntu xenial main') | |
wpilib_apt.close() | |
run(['sudo', 'cp', Path(cache_root, 'wpilib.list'), | |
'/etc/apt/sources.list.d/wpilib.list']) | |
run(['sudo', 'apt-key', 'adv', '--keyserver', | |
'keyserver.ubuntu.com', '--recv-keys', | |
'D51C1F785D5D961140546F3AA06B9F8D55FC4DAE']) | |
artful_apt = open(Path(cache_root, 'artful.list'), | |
'w') | |
artful_apt.write('deb http://cz.archive.ubuntu.com/ubuntu artful main') | |
artful_apt.close() | |
run(['sudo', 'cp', Path(cache_root, 'artful.list'), | |
'/etc/apt/sources.list.d/artful.list']) | |
else: | |
run(['sudo', 'add-apt-repository', 'ppa:wpilib/toolchain', | |
'-y']) | |
apt_update() | |
apt_install('frc-toolchain') | |
# Bazel | |
if not find_application(['bazel', 'version']): | |
LOGGER.info('Installing Bazel...') | |
run(['curl', '-o', | |
Path(cache_root, 'bazel-release.pub.gpg'), | |
'https://bazel.build/bazel-release.pub.gpg']) | |
run(['sudo', 'apt-key', 'add', Path(cache_root, | |
'bazel-release.pub.gpg')]) | |
bazel_apt = open(Path(cache_root, 'bazel.list'), 'w') | |
bazel_apt.write('deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8') | |
bazel_apt.close() | |
run(['sudo', 'cp', Path(cache_root, 'bazel.list'), '/etc/apt/sources.list.d/bazel.list']) | |
apt_update() | |
apt_install('bazel') | |
LOGGER.info('Finished installing dependencies') | |
###################### | |
# Repository Setup # | |
###################### | |
# Ask the user if they want to clone. | |
clone_yn = False | |
if not (options.travis or options.clone_repo): | |
if query_yes_no('Clone {} repository?'.format(options.repo)): | |
LOGGER.debug('User decided to clone') | |
clone_yn = True | |
# Clone the repository | |
if options.clone_repo or clone_yn and not options.travis: | |
github_dir = Path(options.github) | |
run(['mkdir', '-p', github_dir]) | |
repo_url = 'https://github.com/team973/{}'.format(options.repo) | |
repo_dir = Path(github_dir, options.repo) | |
if not repo_dir.is_dir(): | |
LOGGER.info('Cloning %s to %s', options.repo, options.github) | |
run(['git', 'clone', repo_url, repo_dir]) | |
LOGGER.warning( | |
'Cloned using HTTPS. SSH needs additional steps.') | |
else: | |
LOGGER.warning('Repository already exists. Not cloning.') | |
################ | |
# Test Build # | |
################ | |
# Ask the user if they want to build. | |
build_yn = False | |
if not (options.travis or options.build): | |
if query_yes_no('Build {} with Bazel?'.format(options.repo)): | |
LOGGER.debug('User decided to build') | |
build_yn = True | |
# Build | |
if options.build or build_yn or options.travis: | |
LOGGER.info('Building using Bazel') | |
if options.travis: | |
chdir('/build') | |
else: | |
chdir(Path(options.github, options.repo)) | |
if system() == 'Darwin': | |
run(['bazel', 'build', '//src:robot', '--cpu=roborio-darwin']) | |
elif system() == 'Linux': | |
run(['bazel', 'build', '//src:robot', '--cpu=roborio']) | |
############# | |
# Cleanup # | |
############# | |
LOGGER.info('Cleaning up...') | |
rmtree(cache_root) | |
else: | |
raise GeneralError('Platform is not supported') | |
#################### | |
# Main functions # | |
#################### | |
# Main function. | |
def main(args=None): | |
''' | |
Main function that runs at the start of the program. | |
''' | |
########################## | |
# Parser Configuration # | |
########################## | |
# Method handler | |
parser = ArgumentParser(description='Greybots Environement Installer') | |
# Argument handler | |
subparser = parser.add_subparsers(dest='command', help='Commands') | |
subparser.required = True | |
# Shared arguments | |
shared = ArgumentParser(add_help=False) | |
shared.add_argument('-v', '--verbose', | |
help='verbose output', | |
action='store_true') | |
shared.add_argument('-q', '--quiet', | |
help='output only warnings and errors', | |
action='store_true') | |
##################################### | |
# Avaiable Commands Configuration # | |
##################################### | |
# List of methods available to the user. Requires _opts method. | |
command_methods = [ | |
'check', | |
'install' | |
] | |
# Setup avaiable methods | |
for command in command_methods: | |
cmd_method = getattr(sys.modules[__name__], command) | |
method_opt = getattr(sys.modules[__name__], command + '_opts') | |
cmd_parser = subparser.add_parser(command, | |
help=getdoc(cmd_method), | |
parents=[shared]) | |
method_opt(cmd_parser) | |
cmd_parser.set_defaults(cmdobj=cmd_method, github=Path( | |
user_home(), 'Documents', 'GitHub'), repo='greybots-skeleton') | |
# Parse the arguments into an object | |
options = parser.parse_args(args) | |
##################### | |
# Logger Settings # | |
##################### | |
# Logger format | |
log_datefmt = '%H:%M:%S' | |
log_format = '%(asctime)s:%(msecs)03d %(levelname)-8s: %(name)-20s: %(message)s' | |
# Configure Logger | |
basicConfig(datefmt=log_datefmt, format=log_format) | |
# Logger output threshold customization | |
if options.verbose: | |
LOGGER.setLevel(DEBUG) | |
elif options.quiet: | |
LOGGER.setLevel(WARNING) | |
sys.tracebacklimit = 0 | |
else: | |
LOGGER.setLevel(INFO) | |
sys.tracebacklimit = 0 | |
################### | |
# Method Caller # | |
################### | |
# Start the requested script | |
LOGGER.info('Greybots Environement Installer') | |
options.cmdobj(options) | |
# Run the main function at start. | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment