import csv import datetime import re import shutil import subprocess import sys # ====== 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()