From 70115bd7a0a3ba719784e7084afe5ace221202e9 Mon Sep 17 00:00:00 2001 From: James Wiesebron <8340608+jwiesebron@users.noreply.github.com> Date: Thu, 2 Feb 2023 16:26:05 -0800 Subject: [PATCH] Add full automated setup support for M1 macs (#66) ## Summary: - Updates the `install-mac-homebrew` script to set up homebrew correctly for both x86_64 and arm64 - Ensures khan py2 is installed on x84_64 arch - Runs `npm install` for `our-lovely-cli` (not specific to M1 macs, this just isn't covered in onboarding docs or as part of setup scripts) - Check to verify `/opt/homebrew/bin` is before `/usr/local/bin` in `PATH` to ensure arm64 homebrew is used by default I've run this on my machine and everything works, no longer requiring manual work before running the setup scripts. One issue that might come up is related to dotfiles. This will fail on m1 at the step that verifies the correct order of homebrew binaries. A simple solution would be to move the dotfiles setup up to be the first step that's run. Issue: XXX-XXXX ## Test plan: I've tested each change on my machine, but not from a fresh install Author: jwiesebron Reviewers: yogieric, jwiesebron Required Reviewers: Approved By: yogieric Checks: Pull Request URL: https://github.com/Khan/khan-dotfiles/pull/66 --- .gitignore | 2 + bin/install-mac-homebrew.py | 177 +++++++++++++++++++++++++++--------- bin/install-mac-python2.py | 30 +++--- mac-setup-normal.sh | 2 +- setup.sh | 8 +- 5 files changed, 163 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index 253f82c..f99a9ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /**/packer_cache/ /**/builds/ .vscode +.idea* +*.iml \ No newline at end of file diff --git a/bin/install-mac-homebrew.py b/bin/install-mac-homebrew.py index a17c1ab..4b28100 100755 --- a/bin/install-mac-homebrew.py +++ b/bin/install-mac-homebrew.py @@ -4,48 +4,139 @@ # This script will prompt for user's password if sudo access is needed # TODO(ericbrown): Can we check, install & upgrade apps we know we need/want? +import os +import platform import subprocess -HOMEBREW_INSTALLER = \ - 'https://raw.githubusercontent.com/Homebrew/install/master/install.sh' - -print('Checking for mac homebrew') - -install_brew = False -which = subprocess.run(['which', 'brew'], capture_output=True) -if which.returncode != 0: - print('Brew not found, Installing!') - install_brew = True -else: - result = subprocess.run(['brew', '--help'], capture_output=True) - if result.returncode != 0: - print('Brew broken, Re-installing') - install_brew = True - -if install_brew: - # Download installer - installer = subprocess.run(['curl', '-fsSL', HOMEBREW_INSTALLER], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - check=True) - - # Validate that we have sudo access (as installer script checks) - print("This setup script needs your password to install things as root.") - subprocess.run(['sudo', 'sh', '-c', 'echo You have sudo'], check=True) - - # Run downloaded installer - result = subprocess.run(['bash'], input=installer.stdout, check=True) - -print('Updating (but not upgrading) Homebrew') -subprocess.run(['brew', 'update'], capture_output=True, check=True) - -# Install homebrew-cask, so we can use it manage installing binary/GUI apps -# brew tap caskroom/cask - -# Likely need an alternate versions of Casks in order to install chrome-canary -# Required to install chrome-canary -# (Moved to mac-install-apps.sh, but might be needed elsewhere unbeknownst!) -# subprocess.run(['brew', 'tap', 'brew/cask-versions'], check=True) - -# This is where we store our own formula, including a python@2 backport -subprocess.run(['brew', 'tap', 'khan/repo'], check=True) + +class HomebrewInstaller: + HOMEBREW_INSTALLER = ( + "https://raw.githubusercontent.com/Homebrew/install/master/install.sh" + ) + HOMEBREW_UNINSTALLER = ( + "https://raw.githubusercontent.com/Homebrew/install/master/install.sh" + ) + ARM64_BREW_DIR = "/opt/homebrew/bin" + X86_BREW_DIR = "/usr/local/bin" + + def __init__(self): + self.__install_script = None + self.__uninstall_script = None + + @property + def _install_script(self): + if not self.__install_script: + self.__install_script = self._pull_script(self.HOMEBREW_INSTALLER) + return self.__install_script + + @property + def _uninstall_script(self): + if not self.__install_script: + self.__install_script = self._pull_script(self.HOMEBREW_INSTALLER) + return self.__install_script + + @staticmethod + def _pull_script(script_url): + return subprocess.run( + ["curl", "-fsSL", script_url], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=True, + ).stdout + + @staticmethod + def _install_or_uninstall_homebrew(brew_script, force_x86=False): + # Validate that we have sudo access (as installer script checks) + print( + "This setup script needs your password to install things as root." + ) + subprocess.run(["sudo", "sh", "-c", "echo You have sudo"], check=True) + + # Run installer + installer_runner = ( + ["arch", "-x86_64", "/bin/bash"] if force_x86 else ["/bin/bash"] + ) + subprocess.run(installer_runner, input=brew_script, check=True) + + def install_homebrew(self, force_x86=False): + self._install_or_uninstall_homebrew( + brew_script=self._install_script, force_x86=force_x86 + ) + + def uninstall_homebrew(self, force_x86=False): + if force_x86: + os.environ["PATH"] = self.X86_BREW_DIR + os.environ["PATH"] + self._install_or_uninstall_homebrew(brew_script=self._uninstall_script) + if force_x86: + os.environ["PATH"] = self.ARM64_BREW_DIR + os.environ["PATH"] + + def _validate_and_install_homebrew(self, force_x86=False): + brew_runner = ( + ["arch", "-x86_64", "/usr/local/bin/brew"] + if force_x86 + else ["brew"] + ) + if force_x86: + brew_bin_exists = os.path.exists("/usr/local/bin/brew") + else: + brew_bin_exists = ( + subprocess.run( + ["which", "brew"], capture_output=True + ).returncode + == 0 + ) + if not brew_bin_exists: + print("Brew not found, Installing!") + self.install_homebrew(force_x86=force_x86) + else: + result = subprocess.run( + brew_runner + ["--help"], capture_output=True + ) + if result.returncode != 0: + print("Brew broken, Re-installing") + self.uninstall_homebrew(force_x86=force_x86) + self.install_homebrew(force_x86=force_x86) + update_msg = "Updating (but not upgrading) Homebrew" + if force_x86: + update_msg += " x86" + print(update_msg) + subprocess.run( + brew_runner + ["update"], capture_output=True, check=True + ) + + # Install homebrew-cask, so we can use it manage installing binary/GUI + # apps brew tap caskroom/cask + + # Likely need an alternate versions of Casks in order to install + # chrome-canary + # Required to install chrome-canary + # (Moved to mac-install-apps.sh, but might be needed elsewhere + # unbeknownst!) + # subprocess.run(['brew', 'tap', 'brew/cask-versions'], check=True) + + # This is where we store our own formula, including a python@2 backport + subprocess.run(brew_runner + ["tap", "khan/repo"], check=True) + + def validate_and_install_homebrew(self): + self._validate_and_install_homebrew() + + if platform.uname().machine == "arm64": + # Ensure arm64 brew bin is used by default over x86 + path_msg = ( + self.ARM64_BREW_DIR + + "must come before " + + self.X86_BREW_DIR + + " in PATH" + ) + env_path = os.environ["PATH"] + assert self.ARM64_BREW_DIR in env_path, path_msg + opt_homebrew_idx = env_path.index(self.ARM64_BREW_DIR) + usr_local_bin_idx = env_path.index(self.X86_BREW_DIR) + assert opt_homebrew_idx < usr_local_bin_idx, path_msg + # Install x86 brew for M1 architecture to be run with rosetta + self._validate_and_install_homebrew(force_x86=True) + + +if __name__ == "__main__": + print("Checking for mac homebrew") + HomebrewInstaller().validate_and_install_homebrew() diff --git a/bin/install-mac-python2.py b/bin/install-mac-python2.py index 627b63e..747e5a7 100755 --- a/bin/install-mac-python2.py +++ b/bin/install-mac-python2.py @@ -2,38 +2,46 @@ """Install Khan's python2.""" import argparse +import platform import re import subprocess parser = argparse.ArgumentParser() -parser.add_argument("--force", help="Force install of Khan's python2", - action="store_true") +parser.add_argument( + "--force", help="Force install of Khan's python2", action="store_true" +) args = parser.parse_args() -which = subprocess.run(['which', 'python2'], capture_output=True, text=True) -is_installed = (which.returncode == 0 - and which.stdout.strip() != "/usr/bin/python2") +which = subprocess.run(["which", "python2"], capture_output=True, text=True) +is_installed = ( + which.returncode == 0 and which.stdout.strip() != "/usr/bin/python2" +) if is_installed: print("Already running a non-system python2.") if args.force or not is_installed: action = "reinstall" if is_installed else "install" print("Installing python2 from khan/repo. This may take a few minutes.") - subprocess.run(['brew', action, 'khan/repo/python@2'], check=True) + if platform.uname().machine == "arm64": + brew_runner = ["arch", "-x86_64", "/usr/local/bin/brew"] + else: + brew_runner = ["brew"] + subprocess.run(brew_runner + [action, "khan/repo/python@2"], check=True) # Get version of pip2 pip2_version = "" -pip2_version_str = subprocess.run(['pip2', '--version'], - capture_output=True, text=True) +pip2_version_str = subprocess.run( + ["pip2", "--version"], capture_output=True, text=True +) if pip2_version_str: - match = re.match(r'\w+ (\d+)', pip2_version_str.stdout) + match = re.match(r"\w+ (\d+)", pip2_version_str.stdout) if match: pip2_version = match.group(1) if pip2_version and pip2_version > "19": print("Reverting pip2 from version: " + pip2_version_str.stdout.strip()) - subprocess.run(['pip2', 'install', 'pip<20', '-U'], check=True) + subprocess.run(["pip2", "install", "pip<20", "-U"], check=True) # Simple diagnostics -subprocess.run(['pip2', '--version']) +subprocess.run(["pip2", "--version"]) print("which python2: " + which.stdout.strip()) diff --git a/mac-setup-normal.sh b/mac-setup-normal.sh index af3b330..7122dee 100755 --- a/mac-setup-normal.sh +++ b/mac-setup-normal.sh @@ -209,7 +209,7 @@ install_python2() { fi info "Installing python2 from khan/repo. This may take a few minutes." - brew install khan/repo/python@2 + brew86 install khan/repo/python@2 } install_node() { diff --git a/setup.sh b/setup.sh index e91a96d..c31c7f8 100755 --- a/setup.sh +++ b/setup.sh @@ -310,6 +310,11 @@ install_hooks() { fi } +install_our_lovely_cli() { + cd "$DEVTOOLS_DIR/our-lovely-cli" + npm install +} + install_dotfiles check_dependencies @@ -317,8 +322,9 @@ check_dependencies update_userinfo # the order for these is (mostly!) important, beware -clone_repos setup_python +clone_repos +install_our_lovely_cli # pre-req: clone_repos install_and_setup_gcloud # pre-req: setup_python install_deps # pre-reqs: clone_repos, install_and_setup_gcloud install_hooks # pre-req: clone_repos