import csv import subprocess import shutil import datetime import sys import re # ====== Config ====== CSV_FILE = './apps.csv' BREWFILE = './Brewfile' SUPPORT_CONFIG = { 'formula': {'token': 'brew', 'structure': lambda data: f'brew "{data["name"]}"', 'dependency': [] }, 'cask' : {'token': 'cask', 'structure': lambda data: f'cask "{data["name"]}"', 'dependency': [] }, 'mas': {'token': 'mas', 'structure': lambda data: f'mas "{data["name"]}", id: {data["mas_id"]}', 'dependency': [{'name': 'mas', 'script': ['brew', 'install', 'mas']}] }, 'vscode': {'token': 'vscode', 'structure': lambda data: f'vscode "{data["name"]}"', 'dependency': [{'name': 'code', 'script': ['brew', 'install', '--cask', 'visual-studio-code']}] } } MANUAL_TYPE = 'manual' support_types = list(SUPPORT_CONFIG.keys()) def clear(): subprocess.run(['clear']) def check_package(name): path = shutil.which(name) if not path: return False return True def check_brew(): if not check_package('brew'): print('❌ Unable to find brew') sys.exit() def get_csv(): with open(CSV_FILE, 'r', encoding='utf-8-sig') as f: return list(csv.DictReader(f)) def extract_tap(name): parts = name.split('/') if len(parts) == 3: return f'{parts[0]}/{parts[1]}' return None def create_brewfile(): try: with open(BREWFILE, 'r') as old: stamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') with open(f'./Brewfile_{stamp}.backup', 'w') as new: new.writelines(old.readlines()) print(f'📢 Backup old Brewfile (Brewfile_{stamp}.backup)') except FileNotFoundError: pass raw_list = get_csv() temp = {pkg_type:[] for pkg_type in support_types} taps = set() for row in raw_list: pkg_type = row['type'] if pkg_type in temp.keys(): if row['install_on_setup'] == 'yes': tap = extract_tap(row['name']) if tap: taps.add(tap) content = f'# {row["description"]}\n{SUPPORT_CONFIG[pkg_type]["structure"](row)}\n' temp[pkg_type].append(content) elif pkg_type != MANUAL_TYPE: print(f'Error: {row["name"]} {pkg_type}') sys.exit() with open(BREWFILE, 'w') as f: if taps: f.write(f'# {"=" * 15} tap {"=" * 15}\n') for tap in sorted(taps): f.write(f'tap "{tap}"\n') f.write('\n') for pkg_type in support_types: f.write(f'# {"=" * 15} {pkg_type} {"=" * 15}\n') f.writelines(temp[pkg_type]) f.write('\n') print('✅ Successfully created Brewfile') def handle_untrusted_taps(): taps = set() try: with open(BREWFILE, 'r') as f: for line in f: m = re.fullmatch(r'tap "([^/]+/[^/]+)"', line.strip()) if m: taps.add(m.group(1)) except FileNotFoundError: print('❌ Unable to find Brewfile (create it first)') return False if taps: print('⚠️ These third-party taps are needed') for tap in sorted(taps): print(f' - {tap}') print(' These taps is not official and my execute certain script.\n') if input('Trust these taps? (y/N): ').strip().lower() !='y': print('📢 Operation is canceled as these taps are not allowed.') return False for tap in sorted(taps): try: subprocess.run(['brew', 'tap', tap], check=True) subprocess.run(['brew', 'trust', tap], check=True) print(f'✅ Successfully installed tap "{tap}"') except subprocess.CalledProcessError as e: print(f'❌ Failed to install tap "{tap}" ({e.returncode})') return False except FileNotFoundError: print('❌ Unable to find brew') return False return True def get_brewfile_types(): token_to_type = {SUPPORT_CONFIG[t]['token']: t for t in support_types} types = set() try: with open(BREWFILE, 'r') as f: for line in f: token = line.strip().split(' ', 1)[0] if token in token_to_type: types.add(token_to_type[token]) except FileNotFoundError: print('❌ Unable to find Brewfile (create it first)') return types def install_dependencies(): dependencies = [] seen = set() for pkg_type in get_brewfile_types(): for dep in SUPPORT_CONFIG[pkg_type]['dependency']: if dep['name'] not in seen: seen.add(dep['name']) dependencies.append(dep) for dep in dependencies: if check_package(dep['name']): print(f'✅ Dependency "{dep["name"]}" already installed') continue print(f'📢 Installing missing dependency "{dep["name"]}"...') try: subprocess.run(dep['script'], check=True) print(f'✅ Successfully installed dependency "{dep["name"]}"') except subprocess.CalledProcessError as e: print(f'❌ Failed to install dependency "{dep["name"]}" ({e.returncode})') return False except FileNotFoundError: print(f'❌ Unable to find command to install dependency "{dep["name"]}"') return False return True def bundle_install(): if not handle_untrusted_taps(): return if not install_dependencies(): print('❌ Aborting install due to missing dependency') return try: subprocess.run(['brew', 'bundle', 'install', f'--file={BREWFILE}'], check=True) print('✅ Successfully install all packages') except subprocess.CalledProcessError as e: print(f'❌ Failed to install some packages ({e.returncode})') except FileNotFoundError: print('❌ Unable to find brew') def bundle_check(): try: print('📢 Check script running') result = subprocess.run( ['brew', 'bundle', 'check', f'--file={BREWFILE}', '--verbose'] ) if result.returncode == 0: print('✅ All packages are installed') return True else: print('⚠️ There are uninstalled package(s)') return False except FileNotFoundError: print('❌ Unable to find brew') return False def bundle_cleanup(): try: subprocess.run(['brew', 'bundle', 'cleanup', f'--file={BREWFILE}']) if input('⚠️ Are sure to remove? (y/N): ').strip().lower() == 'y': subprocess.run(['brew', 'bundle', 'cleanup', f'--file={BREWFILE}', '--force'], check=True) print('✅ Successfully cleanup') else: print('📢 Canceled') except subprocess.CalledProcessError as e: print(f'❌ Failed to cleanup ({e.returncode})') except FileNotFoundError: print('❌ Unable to find brew') def select_option(): options = [ {'text': 'Create Brewfile', 'func': create_brewfile}, {'text': 'Install packages', 'func': bundle_install}, {'text': 'Check packages', 'func': bundle_check}, {'text': 'Cleanup packages', 'func': bundle_cleanup}, {'text': 'Exit', 'func': sys.exit} ] while True: print('Select option') for idx, row in enumerate(options, 1): print(f' {idx}) {row["text"]}') raw = input('option: ').strip() try: clear() idx = int(raw) - 1 if 0 <= idx < len(options): options[idx]['func']() print() return else: print('⚠️ Write valid number\n') except ValueError: print('⚠️ Write valid number\n') def main(): check_brew() while True: select_option() if __name__ == '__main__': main()