├── .dccache ├── .flake8 ├── .github └── workflows │ └── workflow.yaml ├── .gitignore ├── .pylintrc ├── README.md ├── add.py ├── builder.py ├── check_ratelimit.py ├── config_mgmt.py ├── downloader.py ├── local-test.sh ├── notify.py ├── parallel_check.py ├── plugins.json ├── requirements.txt ├── sort_plugins.py ├── update_config.py └── updater.py /.dccache: -------------------------------------------------------------------------------- 1 | [{"/Users/dhinak/Documents/GitHub/build-repo/.pylintrc":"1","/Users/dhinak/Documents/GitHub/build-repo/add.py":"2","/Users/dhinak/Documents/GitHub/build-repo/builder.py":"3","/Users/dhinak/Documents/GitHub/build-repo/check_ratelimit.py":"4","/Users/dhinak/Documents/GitHub/build-repo/downloader.py":"5","/Users/dhinak/Documents/GitHub/build-repo/import_old.py":"6","/Users/dhinak/Documents/GitHub/build-repo/parallel_check.py":"7","/Users/dhinak/Documents/GitHub/build-repo/sort_plugins.py":"8","/Users/dhinak/Documents/GitHub/build-repo/test_release.py":"9","/Users/dhinak/Documents/GitHub/build-repo/update_config.py":"10","/Users/dhinak/Documents/GitHub/build-repo/updater.py":"11"},[359,1609043345880.1628,"12"],[6382,1609043323409.1133,"13"],[12077,1609043323410.3127,"14"],[217,1609043323410.6653,"15"],[2751,1609043323411.064,"16"],[6461,1609043323411.679,"17"],[1378,1609043323412.0698,"18"],[213,1609043323412.326,"19"],[1799,1609043323412.6558,"20"],[5805,1609043323413.2163,"21"],[6626,1609043323413.8252,"22"],"3317598182cbaacada866694f5ad412226670324676a10d4bdba37ba91d7ab1b","2b6989899fa7539a43eeaf2785f6fe515ae8cc6ad8c372c5a047c49349d8d272","4057a856d5f40a267ef5323de9ceb7027e2f6ca3dacfb7bbe6926885d2044061","edd765034a7f1491508d2062beb6246d73f34f5e51fa5bfa2c9797406f119bfb","505a3205046744882bf07457d8f7aded0d6b2920c5dff7257a5ba9267b89ba18","8d8980ea29ddae82216bafcc0f6a219211a34fd44b3e1a5031809f9c8668b99e","a204565775c3bb211df6792461cef60ac7156cdb368069477efca57696f49346","8ce81cbb63bf426bd2898a084f470805a2ed8e7f6441242900e3432f44bac717","88d36be415255600d3441e6296e11d0c8a2f654fd67aa05196a4b7b6b1b07e62","8c2148e850e5134996a936325d1420540983d24f3cef72d726485d4c07f7647c","e66576fc1a1ff4297d34c401c17e51c4dca9caca3192af4ff0baf001be7801f4"] -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | extend-ignore = E501, E203 -------------------------------------------------------------------------------- /.github/workflows/workflow.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | schedule: 5 | - cron: '*/5 * * * *' 6 | workflow_dispatch: 7 | env: 8 | FORCE_INSTALL: 1 9 | HAS_OPENSSL_BUILD: 1 10 | HAS_OPENSSL_W32BUILD: 0 11 | ACID32: 1 12 | HOMEBREW_NO_INSTALL_CLEANUP: 1 13 | HOMEBREW_NO_AUTO_UPDATE: 1 14 | PROD: ${{ github.ref == 'refs/heads/github-actions' }} 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | jobs: 18 | build: 19 | runs-on: m1_monterey 20 | steps: 21 | - name: Checkout Repository 22 | uses: actions/checkout@v4 23 | # with: 24 | # ref: github-actions 25 | - name: Set up Python 3 26 | run: brew install python3 python-tk 27 | # - uses: actions/setup-python@v4 28 | # with: 29 | # python-version: '3.10' 30 | # cache: pip 31 | - name: Install Python Dependencies 32 | run: | 33 | python3 -m pip install -U pip wheel 34 | python3 -m pip install hammock python-dateutil datetime termcolor2 purl python-magic humanize gitpython cryptography macholib 35 | echo "OVERRIDE_PYTHON3=$(which python3)" >> "$GITHUB_ENV" 36 | # - name: Check Parallel 37 | # run: python3 -u parallel_check.py ${{ secrets.GITHUB_TOKEN }} 38 | - name: Install Build Dependencies 39 | run: | # Needing for VoodooI2C to build without actually having cldoc & cpplint 40 | brew tap FiloSottile/homebrew-musl-cross 41 | brew install libmagic mingw-w64 openssl musl-cross 42 | mkdir wrappers 43 | printf "#!/bin/bash\nexit 0" > wrappers/cldoc 44 | printf "#!/bin/bash\nexit 0" > wrappers/cpplint 45 | chmod +x wrappers/cldoc wrappers/cpplint 46 | echo "$(readlink -f wrappers)" >> "$GITHUB_PATH" 47 | - uses: fregante/setup-git-user@2e28d51939d2a84005a917d2f844090637f435f8 48 | - name: Set Up Working Tree 49 | uses: actions/checkout@v4 50 | with: 51 | ref: builds 52 | path: Config 53 | - name: Check Ratelimit 54 | run: python3 -u check_ratelimit.py ${{ secrets.GITHUB_TOKEN }} 55 | - name: Run Builder 56 | run: python3 -u updater.py ${{ secrets.GITHUB_TOKEN }} ${{ secrets.WEBHOOK_URL }} ${{ secrets.PAYLOAD_KEY }} 57 | env: 58 | JOB_NAME: ${{ github.job }} 59 | - name: Check Ratelimit 60 | run: python3 -u check_ratelimit.py ${{ secrets.GITHUB_TOKEN }} 61 | - name: Upload Artifact 62 | uses: actions/upload-artifact@v4 63 | if: ${{ env.PROD == 'false' }} 64 | with: 65 | name: Build 66 | path: Config 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | gh token.txt 2 | Lilu-and-Friends/ 3 | __pycache__/ 4 | Builds/ 5 | Config/ 6 | Temp/ 7 | .vscode/ 8 | .DS_Store -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))" 4 | 5 | [MESSAGES CONTROL] 6 | 7 | disable=unused-import, 8 | subprocess-run-check, 9 | line-too-long, 10 | too-few-public-methods, 11 | missing-module-docstring, 12 | missing-class-docstring, 13 | missing-function-docstring -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # build-repo 2 | 3 | ![Build](https://github.com/dortania/build-repo/workflows/Build/badge.svg) 4 | 5 | Credit CorpNewt for Lilu & Friends, where some functions originate from and the inspiration for this project. -------------------------------------------------------------------------------- /add.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import hashlib 3 | import json 4 | import os 5 | import time 6 | from pathlib import Path 7 | 8 | import dateutil.parser 9 | import git 10 | import magic 11 | import purl 12 | from hammock import Hammock as hammock 13 | 14 | from config_mgmt import save_config 15 | 16 | mime = magic.Magic(mime=True) 17 | 18 | 19 | def hash_file(file_path: Path): 20 | return hashlib.sha256(file_path.read_bytes()).hexdigest() 21 | 22 | 23 | def expand_globs(str_path: str): 24 | path = Path(str_path) 25 | parts = path.parts[1:] if path.is_absolute() else path.parts 26 | return list(Path(path.root).glob(str(Path("").joinpath(*parts)))) 27 | 28 | 29 | def upload_release_asset(release_id, token, file_path: Path): 30 | upload_url = hammock("https://api.github.com/repos/dortania/build-repo/releases/" + str(release_id), auth=("github-actions", token)).GET().json() 31 | try: 32 | upload_url = upload_url["upload_url"] 33 | except Exception: 34 | print(upload_url) 35 | raise 36 | mime_type = mime.from_file(str(file_path.resolve())) 37 | if not mime_type[0]: 38 | print("Failed to guess mime type!") 39 | raise RuntimeError 40 | 41 | asset_upload = hammock(str(purl.Template(upload_url).expand({"name": file_path.name, "label": file_path.name})), auth=("github-actions", token)).POST( 42 | data=file_path.read_bytes(), 43 | headers={"content-type": mime_type} 44 | ) 45 | return asset_upload.json()["browser_download_url"] 46 | 47 | 48 | def paginate(url, token): 49 | url = hammock(url, auth=("github-actions", token)).GET() 50 | if url.links == {}: 51 | return url.json() 52 | else: 53 | container = url.json() 54 | while url.links.get("next"): 55 | url = hammock(url.links["next"]["url"], auth=("github-actions", token)).GET() 56 | container += url.json() 57 | return container 58 | 59 | 60 | def add_built(plugin, token): 61 | plugin_info = plugin["plugin"] 62 | commit_info = plugin["commit"] 63 | files = plugin["files"] 64 | 65 | script_dir = Path(__file__).parent.absolute() 66 | config_path = script_dir / Path("Config/config.json") 67 | config_path.touch() 68 | config = json.load(config_path.open()) 69 | 70 | name = plugin_info["Name"] 71 | plugin_type = plugin_info.get("Type", "Kext") 72 | 73 | ind = None 74 | 75 | if not config.get(name, None): 76 | config[name] = {} 77 | if not config[name].get("type", None): 78 | config[name]["type"] = plugin_type 79 | if not config[name].get("versions", None): 80 | config[name]["versions"] = [] 81 | 82 | release = {} 83 | if config[name]["versions"]: 84 | config[name]["versions"] = [i for i in config[name]["versions"] if not (i.get("commit", {}).get("sha", None) == commit_info["sha"])] 85 | 86 | release["commit"] = {"sha": commit_info["sha"], "message": commit_info["commit"]["message"], "url": commit_info["html_url"], "tree_url": commit_info["html_url"].replace("/commit/", "/tree/")} 87 | release["version"] = files["version"] 88 | release["date_built"] = datetime.datetime.now(tz=datetime.timezone.utc).isoformat() 89 | release["date_committed"] = dateutil.parser.parse(commit_info["commit"]["committer"]["date"]).isoformat() 90 | release["date_authored"] = dateutil.parser.parse(commit_info["commit"]["author"]["date"]).isoformat() 91 | release["source"] = "built" 92 | 93 | if os.environ.get("PROD", "false") == "true": 94 | releases_url = hammock("https://api.github.com/repos/dortania/build-repo/releases", auth=("github-actions", token)) 95 | 96 | # Delete previous releases 97 | for i in paginate("https://api.github.com/repos/dortania/build-repo/releases", token): 98 | if i["name"] == (name + " " + release["commit"]["sha"][:7]): 99 | print("\tDeleting previous release...") 100 | releases_url(i["id"]).DELETE() 101 | time.sleep(3) # Prevent race conditions 102 | 103 | # Delete tags 104 | check_tag = hammock("https://api.github.com/repos/dortania/build-repo/git/refs/tags/" + name + "-" + release["commit"]["sha"][:7], auth=("github-actions", token)) 105 | if check_tag.GET().status_code != 404: 106 | print("\tDeleting previous tag...") 107 | check_tag.DELETE() 108 | time.sleep(3) # Prevent race conditions 109 | 110 | # Create release 111 | create_release = releases_url.POST(json={ 112 | "tag_name": name + "-" + release["commit"]["sha"][:7], 113 | "target_commitish": "builds", 114 | "name": name + " " + release["commit"]["sha"][:7] 115 | }) 116 | # print(create_release.json()["id"]) 117 | release["release"] = {"id": create_release.json()["id"], "url": create_release.json()["html_url"]} 118 | 119 | if not release.get("hashes", None): 120 | release["hashes"] = {"debug": {"sha256": ""}, "release": {"sha256": ""}} 121 | 122 | release["hashes"]["debug"] = {"sha256": hash_file(files["debug"])} 123 | release["hashes"]["release"] = {"sha256": hash_file(files["release"])} 124 | 125 | if files["extras"]: 126 | for file in files["extras"]: 127 | release["hashes"][file.name] = {"sha256": hash_file(file)} 128 | 129 | if os.environ.get("PROD", "false") == "true": 130 | if not release.get("links", None): 131 | release["links"] = {} 132 | 133 | for i in ["debug", "release"]: 134 | release["links"][i] = upload_release_asset(release["release"]["id"], token, files[i]) 135 | 136 | if files["extras"]: 137 | if not release.get("extras", None): 138 | release["extras"] = {} 139 | for file in files["extras"]: 140 | release["extras"][file.name] = upload_release_asset(release["release"]["id"], token, file) 141 | new_line = "\n" # No escapes in f-strings 142 | 143 | release["release"]["description"] = f"""**Changes:** 144 | {release['commit']['message'].strip()} 145 | [View on GitHub]({release['commit']['url']}) ([browse tree]({release['commit']['tree_url']})) 146 | 147 | **Hashes**: 148 | **Debug:** 149 | {files["debug"].name + ': ' + release['hashes']['debug']["sha256"]} 150 | **Release:** 151 | {files["release"].name + ': ' + release['hashes']['release']["sha256"]} 152 | {'**Extras:**' if files["extras"] else ''} 153 | {new_line.join([(file.name + ': ' + release['hashes'][file.name]['sha256']) for file in files["extras"]]) if files["extras"] else ''} 154 | """.strip() 155 | 156 | hammock("https://api.github.com/repos/dortania/build-repo/releases/" + str(release["release"]["id"]), auth=("github-actions", token)).POST(json={ 157 | "body": release["release"]["description"] 158 | }) 159 | 160 | config[name]["versions"].insert(0, release) 161 | config[name]["versions"].sort(key=lambda x: (x["date_committed"], x["date_authored"]), reverse=True) 162 | save_config(config) 163 | 164 | if os.environ.get("PROD", "false") == "true": 165 | repo = git.Repo(script_dir / Path("Config")) 166 | repo.git.add(all=True) 167 | repo.git.commit(message="Deploying to builds") 168 | repo.git.push() 169 | 170 | return release 171 | -------------------------------------------------------------------------------- /builder.py: -------------------------------------------------------------------------------- 1 | import io 2 | import plistlib 3 | import shutil 4 | import stat 5 | import subprocess 6 | import zipfile 7 | from os import chdir 8 | from pathlib import Path 9 | 10 | from hammock import Hammock as hammock 11 | 12 | 13 | class Builder: 14 | def __init__(self): 15 | self.lilu = {} 16 | self.clang32 = None 17 | self.edk2 = None 18 | self.script_dir = Path(__file__).parent.absolute() 19 | 20 | self.working_dir = self.script_dir / Path("Temp") 21 | if self.working_dir.exists(): 22 | shutil.rmtree(self.working_dir) 23 | self.working_dir.mkdir() 24 | 25 | self.build_dir = self.script_dir / Path("Builds") 26 | if self.build_dir.exists(): 27 | shutil.rmtree(self.build_dir) 28 | self.build_dir.mkdir() 29 | 30 | @staticmethod 31 | def _expand_globs(p: str): 32 | if "*" in p: 33 | path = Path(p) 34 | parts = path.parts[1:] if path.is_absolute() else path.parts 35 | return list(Path(path.root).glob(str(Path("").joinpath(*parts)))) 36 | else: 37 | return [Path(p)] 38 | 39 | def _bootstrap_clang32(self, target_dir: Path): 40 | chdir(self.working_dir) 41 | clang_dir = self.working_dir / Path("clang32") 42 | 43 | if not self.clang32: 44 | print("Bootstrapping prerequisite: clang32...") 45 | if clang_dir.exists(): 46 | shutil.rmtree(clang_dir) 47 | clang_dir.mkdir() 48 | chdir(clang_dir) 49 | print("\tDownloading clang32 binary...") 50 | zipfile.ZipFile(io.BytesIO(hammock("https://github.com/acidanthera/ocbuild/releases/download/llvm-kext32-latest/clang-12.zip").GET().content)).extractall() 51 | (clang_dir / Path("clang-12")).chmod((clang_dir / Path("clang-12")).stat().st_mode | stat.S_IEXEC) 52 | 53 | print("\tDownloading clang32 scripts...") 54 | for tool in ["fix-macho32", "libtool32"]: 55 | tool_path = Path(tool) 56 | tool_path.write_bytes(hammock(f"https://raw.githubusercontent.com/acidanthera/ocbuild/master/scripts/{tool}").GET().content) 57 | tool_path.chmod(tool_path.stat().st_mode | stat.S_IEXEC) 58 | self.clang32 = clang_dir.resolve() 59 | (target_dir / Path("clang32")).symlink_to(self.clang32) 60 | 61 | def _bootstrap_edk2(self): 62 | chdir(self.working_dir) 63 | if not self.edk2: 64 | print("Bootstrapping prerequisite: EDK II...") 65 | if Path("edk2").exists(): 66 | shutil.rmtree(Path("edk2")) 67 | print("\tCloning the repo...") 68 | result = subprocess.run("git clone https://github.com/acidanthera/audk edk2 --branch master --depth 1".split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 69 | if result.returncode != 0: 70 | print("\tClone failed!") 71 | print(result.stdout.decode()) 72 | return False 73 | self.edk2 = True 74 | 75 | def _build_lilu(self): 76 | chdir(self.working_dir) 77 | if not self.lilu: 78 | print("Building prerequiste: Lilu...") 79 | if Path("Lilu").exists(): 80 | shutil.rmtree(Path("Lilu")) 81 | print("\tCloning the repo...") 82 | result = subprocess.run("git clone https://github.com/acidanthera/Lilu.git".split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 83 | if result.returncode != 0: 84 | print("\tClone failed!") 85 | print(result.stdout.decode()) 86 | return False 87 | chdir(self.working_dir / Path("Lilu")) 88 | print("\tCloning MacKernelSDK...") 89 | result = subprocess.run("git clone https://github.com/acidanthera/MacKernelSDK.git".split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 90 | if result.returncode != 0: 91 | print("\tClone of MacKernelSDK failed!") 92 | print(result.stdout.decode()) 93 | return False 94 | self._bootstrap_clang32(self.working_dir / Path("Lilu")) 95 | chdir(self.working_dir / Path("Lilu")) 96 | print("\tBuilding debug version...") 97 | result = subprocess.run("xcodebuild -quiet -configuration Debug".split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 98 | if result.returncode != 0: 99 | print("\tBuild failed!") 100 | print(result.stdout.decode()) 101 | return False 102 | result = subprocess.run("git rev-parse HEAD".split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 103 | if result.returncode != 0: 104 | print("\tObtaining commit hash failed!") 105 | print(result.stdout.decode()) 106 | return False 107 | else: 108 | commithash = result.stdout.decode().strip() 109 | shutil.copytree(Path("build/Debug/Lilu.kext"), self.working_dir / Path("Lilu.kext")) 110 | self.lilu = [commithash, self.working_dir / Path("Lilu.kext")] 111 | return self.lilu[1] 112 | 113 | def build(self, plugin, commithash=None): 114 | name = plugin["Name"] 115 | url = plugin["URL"] 116 | needs_lilu = plugin.get("Lilu", False) 117 | needs_mackernelsdk = plugin.get("MacKernelSDK", False) 118 | fat = plugin.get("32-bit", False) 119 | edk2 = plugin.get("EDK II", False) 120 | command = plugin.get("Command") 121 | prebuild = plugin.get("Pre-Build", []) 122 | postbuild = plugin.get("Post-Build", []) 123 | build_opts = plugin.get("Build Opts", []) 124 | build_dir = plugin.get("Build Dir", "build/") 125 | p_info = plugin.get("Info", f"{build_dir}Release/{name}.kext/Contents/Info.plist") 126 | b_type = plugin.get("Type", "Kext") 127 | d_file = plugin.get("Debug File", f"{build_dir}Debug/*.kext") 128 | r_file = plugin.get("Release File", f"{build_dir}Release/*.kext") 129 | extra_files = plugin.get("Extras", None) 130 | v_cmd = plugin.get("Version", None) 131 | 132 | chdir(self.working_dir) 133 | 134 | if needs_lilu: 135 | if not self._build_lilu(): 136 | print("Building of prerequiste: Lilu failed!") 137 | return False 138 | 139 | chdir(self.working_dir) 140 | print("Building " + name + "...") 141 | if Path(name).exists(): 142 | shutil.rmtree(Path(name)) 143 | print("\tCloning the repo...") 144 | result = subprocess.run(["git", "clone", "--recurse-submodules", url + ".git", name], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 145 | if result.returncode != 0: 146 | print("\tClone failed!") 147 | print(result.stdout.decode()) 148 | return False 149 | chdir(self.working_dir / Path(name)) 150 | 151 | if commithash: 152 | print("\tChecking out to " + commithash + "...") 153 | result = subprocess.run(["git", "checkout", commithash], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 154 | if result.returncode != 0: 155 | print("\tCheckout failed!") 156 | print(result.stdout.decode()) 157 | return False 158 | else: 159 | result = subprocess.run("git rev-parse HEAD".split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 160 | if result.returncode != 0: 161 | print("\tObtaining commit hash failed!") 162 | print(result.stdout.decode()) 163 | return False 164 | else: 165 | commithash = result.stdout.decode().strip() 166 | chdir(self.working_dir / Path(name)) 167 | 168 | if needs_lilu: 169 | lilu_path = self._build_lilu() 170 | if not lilu_path: 171 | print("Building of prerequiste: Lilu failed!") 172 | return False 173 | shutil.copytree(lilu_path, self.working_dir / Path(name) / Path("Lilu.kext")) 174 | 175 | chdir(self.working_dir / Path(name)) 176 | if needs_mackernelsdk: 177 | print("\tCloning MacKernelSDK...") 178 | result = subprocess.run("git clone https://github.com/acidanthera/MacKernelSDK.git".split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 179 | if result.returncode != 0: 180 | print("\tClone of MacKernelSDK failed!") 181 | print(result.stdout.decode()) 182 | return False 183 | 184 | chdir(self.working_dir / Path(name)) 185 | if fat: 186 | self._bootstrap_clang32(self.working_dir / Path(name)) 187 | build_opts += ["-arch", "x86_64", "-arch", "ACID32"] 188 | 189 | chdir(self.working_dir / Path(name)) 190 | if edk2: 191 | self._bootstrap_edk2() 192 | 193 | chdir(self.working_dir / Path(name)) 194 | if prebuild: 195 | print("\tRunning prebuild tasks...") 196 | for task in prebuild: 197 | print("\t\tRunning task '" + task["name"] + "'") 198 | args = [task["path"]] 199 | args.extend(task["args"]) 200 | result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 201 | if result.returncode != 0: 202 | print("\t\tTask failed!") 203 | print(result.stdout.decode()) 204 | return False 205 | else: 206 | print("\t\tTask completed.") 207 | chdir(self.working_dir / Path(name)) 208 | if isinstance(command, str) or (isinstance(command, list) and all(isinstance(n, str) for n in command)): 209 | print("\tBuilding...") 210 | if isinstance(command, str): 211 | command = command.split() 212 | result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 213 | if result.returncode != 0: 214 | print("\tBuild failed!") 215 | print(result.stdout.decode()) 216 | print("\tReturn code: " + str(result.returncode)) 217 | return False 218 | elif isinstance(command, list) and all(isinstance(n, dict) for n in command): 219 | # Multiple commands 220 | for i in command: 221 | print("\t" + i["name"] + "...") 222 | result = subprocess.run([i["path"]] + i["args"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 223 | if result.returncode != 0: 224 | print("\tCommand failed!") 225 | print(result.stdout.decode()) 226 | print("\tReturn code: " + str(result.returncode)) 227 | return False 228 | else: 229 | print("\tBuilding release version...") 230 | args = "xcodebuild -quiet -configuration Release".split() 231 | args += build_opts 232 | args += ["-jobs", "1"] 233 | # BUILD_DIR should only be added if we don't have scheme. Otherwise, use -derivedDataPath 234 | args += ["-derivedDataPath", "build"] if "-scheme" in build_opts else ["BUILD_DIR=build/"] 235 | 236 | result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 237 | if result.returncode != 0: 238 | print("\tBuild failed!") 239 | print(result.stdout.decode()) 240 | print("\tReturn code: " + str(result.returncode)) 241 | return False 242 | 243 | print("\tBuilding debug version...") 244 | args = "xcodebuild -quiet -configuration Debug".split() 245 | args += build_opts 246 | args += ["-jobs", "1"] 247 | # BUILD_DIR should only be added if we don't have scheme. Otherwise, use -derivedDataPath 248 | args += ["-derivedDataPath", "build"] if "-scheme" in build_opts else ["BUILD_DIR=build/"] 249 | 250 | result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 251 | if result.returncode != 0: 252 | print("\tBuild failed!") 253 | print(result.stdout.decode()) 254 | print("\tReturn code: " + str(result.returncode)) 255 | return False 256 | chdir(self.working_dir / Path(name)) 257 | if postbuild: 258 | print("\tRunning postbuild tasks...") 259 | for task in postbuild: 260 | print("\t\tRunning task '" + task["name"] + "'") 261 | args = [task["path"]] 262 | args.extend(task["args"]) 263 | result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=task.get("cwd", None)) 264 | if result.returncode != 0: 265 | print("\t\tTask failed!") 266 | print(result.stdout.decode()) 267 | return False 268 | else: 269 | print("\t\tTask completed.") 270 | chdir(self.working_dir / Path(name)) 271 | if v_cmd: 272 | if isinstance(v_cmd, str): 273 | v_cmd = v_cmd.split() 274 | result = subprocess.run(v_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 275 | if result.returncode != 0: 276 | print("\tRunning version command failed!") 277 | print(result.stdout.decode()) 278 | return False 279 | else: 280 | version = result.stdout.decode().strip() 281 | elif b_type == "Kext": 282 | plistpath = Path(p_info) 283 | version = plistlib.load(plistpath.open(mode="rb"))["CFBundleVersion"] 284 | else: 285 | print("\tNo version command!") 286 | return False 287 | print("\tVersion: " + version) 288 | category_type = {"Kext": "Kexts", "Bootloader": "Bootloaders", "Utility": "Utilities", "Other": "Others"}[b_type] 289 | print("\tCopying to build directory...") 290 | extras = [] 291 | # (extras.extend(self._expand_globs(i)) for i in extra_files) if extra_files is not None else None # pylint: disable=expression-not-assigned 292 | if extra_files is not None: 293 | for i in extra_files: 294 | extras.extend(self._expand_globs(i)) 295 | debug_file = self._expand_globs(d_file)[0] 296 | release_file = self._expand_globs(r_file)[0] 297 | debug_dir = self.build_dir / Path(category_type) / Path(name) / Path(commithash) / Path("Debug") 298 | release_dir = self.build_dir / Path(category_type) / Path(name) / Path(commithash) / Path("Release") 299 | for directory in [debug_dir, release_dir]: 300 | if directory.exists(): 301 | shutil.rmtree(directory) 302 | directory.mkdir(parents=True) 303 | if extras: 304 | for i in extras: 305 | if i.is_dir(): 306 | print(f"\t{i} is a dir; please fix!") 307 | shutil.copytree(i, debug_dir / i.name) 308 | shutil.copytree(i, release_dir / i.name) 309 | elif i.is_file(): 310 | shutil.copy(i, debug_dir) 311 | shutil.copy(i, release_dir) 312 | elif not i.exists(): 313 | print(f"\t{i} does not exist!") 314 | return False 315 | else: 316 | print(f"\t{i} is not a dir or a file!") 317 | continue 318 | 319 | if debug_file.is_dir(): 320 | print(f"{debug_file} is a dir; please fix!") 321 | shutil.copytree(debug_file, debug_dir / debug_file.name) 322 | elif debug_file.is_file(): 323 | shutil.copy(debug_file, debug_dir) 324 | 325 | if release_file.is_dir(): 326 | print(f"{release_file} is a dir; please fix!") 327 | shutil.copytree(release_file, release_dir / release_file.name) 328 | elif release_file.is_file(): 329 | shutil.copy(release_file, release_dir) 330 | 331 | return {"debug": debug_dir / Path(debug_file.name), "release": release_dir / Path(release_file.name), "extras": [debug_dir / Path(i.name) for i in extras], "version": version} 332 | -------------------------------------------------------------------------------- /check_ratelimit.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from hammock import Hammock as hammock 3 | 4 | token = sys.argv[1].strip() 5 | eee = hammock("https://api.github.com/rate_limit").GET(auth=("github-actions", token)) 6 | print(eee.text or eee.content) 7 | -------------------------------------------------------------------------------- /config_mgmt.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | from pathlib import Path 4 | 5 | 6 | def save_config(data: dict): 7 | config_dir = Path(__file__).parent.absolute() / Path("Config") 8 | plugin_dir = config_dir / Path("plugins") 9 | plugin_dir.mkdir(exist_ok=True) 10 | 11 | version = data["_version"] 12 | 13 | for plugin in data: 14 | if plugin == "_version": 15 | continue 16 | data[plugin]["versions"].sort(key=lambda x: (x["date_committed"], x["date_authored"]), reverse=True) 17 | json.dump(data[plugin] | {"_version": version}, (plugin_dir / Path(f"{plugin}.json")).open("w"), sort_keys=True) 18 | 19 | json.dump(data, (config_dir / Path("config.json")).open("w"), sort_keys=True) 20 | 21 | latest = copy.deepcopy(data) 22 | for plugin in latest: 23 | if plugin == "_version": 24 | continue 25 | latest[plugin]["versions"] = [latest[plugin]["versions"][0]] 26 | 27 | json.dump(latest, (config_dir / Path("latest.json")).open("w"), sort_keys=True) 28 | json.dump({"plugins": list(data.keys()), "_version": version}, (config_dir / Path("plugins.json")).open("w"), sort_keys=True) 29 | -------------------------------------------------------------------------------- /downloader.py: -------------------------------------------------------------------------------- 1 | import json 2 | import distutils.util 3 | import zipfile 4 | from pathlib import Path 5 | from hammock import Hammock as hammock 6 | 7 | plugins = hammock("https://raw.githubusercontent.com/dortania/build-repo/github-actions/plugins.json").GET() 8 | plugins = json.loads(plugins.text) 9 | 10 | config = hammock("https://raw.githubusercontent.com/dortania/build-repo/builds/config.json").GET() 11 | config = json.loads(config.text) 12 | print("Global Settings: ") 13 | ensure_latest = bool(distutils.util.strtobool(input("Ensure latest? (\"true\" or \"false\") ").lower())) 14 | unzip = bool(distutils.util.strtobool(input("Unzip automatically and delete zip? (\"true\" or \"false\") ").lower())) 15 | extract_dir = input("Put files in directory (leave blank for current dir): ") if unzip else None 16 | dbg = input("Debug or release? (\"debug\" or \"release\") ").lower() 17 | while True: 18 | target = input("Enter product to download (case sensitive): ") 19 | try: 20 | if ensure_latest: 21 | organization = repo = None 22 | for plugin in plugins["Plugins"]: 23 | if plugin["Name"] == target: 24 | organization, repo = plugin["URL"].strip().replace("https://github.com/", "").split("/") 25 | break 26 | if not repo: 27 | print("Product " + target + " not available\n") 28 | continue 29 | commits_url = hammock("https://api.github.com").repos(organization, repo).commits.GET(params={"per_page": 100}) 30 | commit_hash = json.loads(commits_url.text or commits_url.content)[0]["sha"] 31 | to_dl = None 32 | for i in config[target]["versions"]: 33 | if i["commit"]["sha"] == commit_hash: 34 | to_dl = i 35 | break 36 | if not to_dl: 37 | print("Latest version (" + commit_hash + ") unavailable\n") 38 | continue 39 | else: 40 | to_dl = config[target]["versions"][0] 41 | dl_link = to_dl["links"][dbg] 42 | print(f"Downloading {target} version {to_dl['version']} sha {to_dl['commit']['sha']} and date built {to_dl['date_built']}") 43 | except KeyError as error: 44 | if error.args[0] == target: 45 | print("Product " + error.args[0] + " not available\n") 46 | continue 47 | elif error.args[0] == dbg: 48 | print("Version " + error.args[0] + " not available\n") 49 | continue 50 | else: 51 | raise error 52 | file_name = Path(dl_link).name 53 | dl_url = hammock(dl_link).GET() 54 | Path(file_name).write_bytes(dl_url.content or dl_url.text) 55 | print("Finished downloading.") 56 | if unzip: 57 | with zipfile.ZipFile(file_name, "r") as zip_ref: 58 | zip_ref.extractall(extract_dir) 59 | Path(file_name).unlink() 60 | print("Finished extracting.") 61 | print("Done.\n") 62 | -------------------------------------------------------------------------------- /local-test.sh: -------------------------------------------------------------------------------- 1 | pip3 install hammock python-dateutil datetime termcolor purl python-magic 2 | rm -Rf Config Temp Builds 3 | git clone https://github.com/dortania/build-repo.git Config --depth 1 --single-branch --branch builds --sparse --filter=blob:none 4 | python3 -u check_ratelimit.py 5 | python3 -u updater.py 6 | python3 -u check_ratelimit.py 7 | python3 -u update_config.py -------------------------------------------------------------------------------- /notify.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | 5 | import cryptography.fernet as fernet 6 | import requests 7 | from hammock import Hammock as hammock 8 | 9 | JOB_LINK = None 10 | 11 | webhook = sys.argv[2].strip() 12 | fern = fernet.Fernet(sys.argv[3].strip().encode()) 13 | 14 | 15 | def get_current_run_link(token): 16 | global JOB_LINK 17 | if JOB_LINK: 18 | return JOB_LINK 19 | this_run = hammock(f"https://api.github.com/repos/{os.environ['GITHUB_REPOSITORY']}/actions/runs/{os.environ['GITHUB_RUN_ID']}/jobs", auth=("github-actions", token)).GET() 20 | try: 21 | this_run.raise_for_status() 22 | except requests.HTTPError as err: 23 | print(err) 24 | return 25 | this_job = [i for i in this_run.json()["jobs"] if i["name"] == os.environ['JOB_NAME']][0] 26 | JOB_LINK = this_job["html_url"] 27 | return JOB_LINK 28 | 29 | 30 | def notify(token, results, status): 31 | if os.environ.get("PROD", "false") == "true": 32 | results = dict(results) 33 | results["status"] = status 34 | results["job_url"] = get_current_run_link(token) 35 | if results.get("files"): 36 | results["files"] = {k: str(v) for k, v in results["files"].items()} 37 | 38 | requests.post(webhook, data=fern.encrypt(json.dumps(results).encode())) 39 | 40 | 41 | def notify_success(token, results): 42 | notify(token, results, "succeeded") 43 | 44 | 45 | def notify_failure(token, results): 46 | notify(token, results, "failed") 47 | 48 | 49 | def notify_error(token, results): 50 | notify(token, results, "errored") 51 | -------------------------------------------------------------------------------- /parallel_check.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | 5 | import requests 6 | 7 | token = sys.argv[1].strip() 8 | 9 | session = requests.Session() 10 | session.auth = ("github-actions", token) 11 | 12 | this_run_url = f"https://api.github.com/repos/{os.environ['GITHUB_REPOSITORY']}/actions/runs/{os.environ['GITHUB_RUN_ID']}" 13 | workflow_url = session.get(this_run_url).json()["workflow_url"] 14 | 15 | runs = session.get(f"{workflow_url}/runs").json() 16 | run_index = 0 17 | 18 | for i, run in enumerate(runs["workflow_runs"]): 19 | if str(run["id"]) == str(os.environ["GITHUB_RUN_ID"]): 20 | run_index = i 21 | break 22 | 23 | for i, run in enumerate(runs["workflow_runs"]): 24 | if i > run_index and str(run["id"]) != str(os.environ["GITHUB_RUN_ID"]) and run["status"] != "completed": 25 | print(f"Another build ({run['id']} with status {run['status']}) is running, cancelling this one...") 26 | cancel_request = session.post(f"{this_run_url}/cancel") 27 | if cancel_request.status_code != 202: 28 | sys.exit(f"Status code did not match: {cancel_request.status_code}") 29 | else: 30 | print("Cancel request acknowledged, sleeping 10 seconds to account for delay...") 31 | time.sleep(10) 32 | sys.exit(0) 33 | -------------------------------------------------------------------------------- /plugins.json: -------------------------------------------------------------------------------- 1 | { 2 | "Plugins": [ 3 | { 4 | "Debug File": "build/Debug/*.zip", 5 | "Desc": "An open source kernel extension providing a set of patches required for non-native Airport Broadcom Wi-Fi cards.", 6 | "Lilu": true, 7 | "MacKernelSDK": true, 8 | "Name": "AirportBrcmFixup", 9 | "Release File": "build/Release/*.zip", 10 | "URL": "https://github.com/acidanthera/AirportBrcmFixup" 11 | }, 12 | { 13 | "Debug File": "build/Debug/*.zip", 14 | "Desc": "dynamic audio patching", 15 | "Lilu": true, 16 | "MacKernelSDK": true, 17 | "Name": "AppleALC", 18 | "Release File": "build/Release/*.zip", 19 | "URL": "https://github.com/acidanthera/AppleALC" 20 | }, 21 | { 22 | "Build Opts": [ 23 | "-target", 24 | "Package" 25 | ], 26 | "Debug File": "build/Debug/*.zip", 27 | "Desc": "An open source kernel extension which applies PatchRAM updates for Broadcom RAMUSB based devices", 28 | "Lilu": true, 29 | "MacKernelSDK": true, 30 | "Name": "BrcmPatchRAM", 31 | "Release File": "build/Release/*.zip", 32 | "URL": "https://github.com/acidanthera/BrcmPatchRAM" 33 | }, 34 | { 35 | "Debug File": "build/Debug/*.zip", 36 | "Desc": "Handler for brightness keys without DSDT patches", 37 | "Lilu": true, 38 | "MacKernelSDK": true, 39 | "Name": "BrightnessKeys", 40 | "Release File": "build/Release/*.zip", 41 | "URL": "https://github.com/acidanthera/BrightnessKeys" 42 | }, 43 | { 44 | "Debug File": "build/Debug/*.zip", 45 | "Desc": "Dynamic macOS CPU power management data injection", 46 | "Lilu": true, 47 | "MacKernelSDK": true, 48 | "Name": "CPUFriend", 49 | "Release File": "build/Release/*.zip", 50 | "URL": "https://github.com/acidanthera/CPUFriend" 51 | }, 52 | { 53 | "Debug File": "build/Debug/*.zip", 54 | "Desc": "Combines functionality of VoodooTSCSync and disabling xcpm_urgency if TSC is not in sync", 55 | "Lilu": true, 56 | "MacKernelSDK": true, 57 | "Name": "CpuTscSync", 58 | "Release File": "build/Release/*.zip", 59 | "URL": "https://github.com/acidanthera/CpuTscSync" 60 | }, 61 | { 62 | "Debug File": "build/Debug/*.zip", 63 | "Desc": "Various patches to install Rosetta cryptex", 64 | "Lilu": true, 65 | "MacKernelSDK": true, 66 | "Name": "CryptexFixup", 67 | "Release File": "build/Release/*.zip", 68 | "URL": "https://github.com/acidanthera/CryptexFixup" 69 | }, 70 | { 71 | "Debug File": "build/Debug/*.zip", 72 | "Desc": "A Lilu plugin intended to enable debug output in the macOS kernel", 73 | "Lilu": true, 74 | "MacKernelSDK": true, 75 | "Name": "DebugEnhancer", 76 | "Release File": "build/Release/*.zip", 77 | "URL": "https://github.com/acidanthera/DebugEnhancer" 78 | }, 79 | { 80 | "Debug File": "build/Debug/*.zip", 81 | "Desc": "Allows reading Embedded Controller fields over 1 byte long", 82 | "Lilu": true, 83 | "MacKernelSDK": true, 84 | "Name": "ECEnabler", 85 | "Release File": "build/Release/*.zip", 86 | "URL": "https://github.com/1Revenger1/ECEnabler" 87 | }, 88 | { 89 | "Debug File": "build/Debug/*.zip", 90 | "Desc": "SD host controller support for macOS", 91 | "Lilu": true, 92 | "MacKernelSDK": true, 93 | "Name": "EmeraldSDHC", 94 | "Release File": "build/Release/*.zip", 95 | "URL": "https://github.com/acidanthera/EmeraldSDHC" 96 | }, 97 | { 98 | "Debug File": "build/Debug/*.zip", 99 | "Desc": "Lilu Kernel extension for enabling Sidecar, NightShift, AirPlay to Mac and Universal Control support", 100 | "Lilu": true, 101 | "MacKernelSDK": true, 102 | "Name": "FeatureUnlock", 103 | "Release File": "build/Release/*.zip", 104 | "URL": "https://github.com/acidanthera/FeatureUnlock" 105 | }, 106 | { 107 | "Debug File": "build/Debug/*.zip", 108 | "Desc": "A Lilu plugin intended to fix hibernation compatibility issues", 109 | "Lilu": true, 110 | "MacKernelSDK": true, 111 | "Name": "HibernationFixup", 112 | "Release File": "build/Release/*.zip", 113 | "URL": "https://github.com/acidanthera/HibernationFixup" 114 | }, 115 | { 116 | "Build Opts": [ 117 | "-alltargets" 118 | ], 119 | "Debug File": "build/Debug/*.zip", 120 | "Desc": "Intel Bluetooth Drivers for macOS", 121 | "Lilu": true, 122 | "MacKernelSDK": true, 123 | "Name": "IntelBluetoothFirmware", 124 | "Release File": "build/Release/*.zip", 125 | "URL": "https://github.com/OpenIntelWireless/IntelBluetoothFirmware" 126 | }, 127 | { 128 | "Debug File": "build/Debug/*.zip", 129 | "Desc": "Intel Ethernet LAN driver for macOS", 130 | "Lilu": true, 131 | "MacKernelSDK": true, 132 | "Name": "IntelMausi", 133 | "Release File": "build/Release/*.zip", 134 | "URL": "https://github.com/acidanthera/IntelMausi" 135 | }, 136 | { 137 | "32-bit": true, 138 | "Debug File": "build/Debug/*.zip", 139 | "Desc": "for arbitrary kext, library, and program patching", 140 | "MacKernelSDK": true, 141 | "Name": "Lilu", 142 | "Release File": "build/Release/*.zip", 143 | "URL": "https://github.com/acidanthera/Lilu" 144 | }, 145 | { 146 | "Build Opts": [ 147 | "-target", 148 | "Package" 149 | ], 150 | "Debug File": "build/Debug/*.zip", 151 | "Desc": "Hyper-V integration support for macOS", 152 | "Lilu": true, 153 | "MacKernelSDK": true, 154 | "Name": "MacHyperVSupport", 155 | "Release File": "build/Release/*.zip", 156 | "URL": "https://github.com/acidanthera/MacHyperVSupport" 157 | }, 158 | { 159 | "Debug File": "build/Debug/*.zip", 160 | "Desc": "patches for the Apple NVMe storage driver, IONVMeFamily", 161 | "Lilu": true, 162 | "MacKernelSDK": true, 163 | "Name": "NVMeFix", 164 | "Release File": "build/Release/*.zip", 165 | "URL": "https://github.com/acidanthera/NVMeFix" 166 | }, 167 | { 168 | "Command": [ 169 | { 170 | "args": [], 171 | "name": "Building DuetPkg", 172 | "path": "./build_duet.tool" 173 | }, 174 | { 175 | "args": [], 176 | "name": "Building OpenCorePkg", 177 | "path": "./build_oc.tool" 178 | } 179 | ], 180 | "Debug File": "Binaries/*DEBUG*.zip", 181 | "Desc": "OpenCore front end", 182 | "Max Per Run": 2, 183 | "Name": "OpenCorePkg", 184 | "Release File": "Binaries/*RELEASE*.zip", 185 | "Type": "Bootloader", 186 | "URL": "https://github.com/acidanthera/OpenCorePkg", 187 | "Version": [ 188 | "awk", 189 | "/^#define OPEN_CORE_VERSION/ { print substr($3,2,5) }", 190 | "Include/Acidanthera/Library/OcMainLib.h" 191 | ] 192 | }, 193 | { 194 | "Debug File": "build/Debug/*.zip", 195 | "Desc": "open source kernel extension providing a way to emulate some offsets in your CMOS (RTC) memory", 196 | "Lilu": true, 197 | "MacKernelSDK": true, 198 | "Name": "RTCMemoryFixup", 199 | "Release File": "build/Release/*.zip", 200 | "URL": "https://github.com/acidanthera/RTCMemoryFixup" 201 | }, 202 | { 203 | "Debug File": "debug.zip", 204 | "Desc": "OS X open source driver for the Realtek RTL8111/8168 family", 205 | "Name": "RealtekRTL8111", 206 | "MacKernelSDK": true, 207 | "Post-Build": [ 208 | { 209 | "args": [ 210 | "-r", 211 | "-X", 212 | "../../release.zip", 213 | "RealtekRTL8111.kext" 214 | ], 215 | "cwd": "build/Release", 216 | "name": "Zip Release Directory", 217 | "path": "zip" 218 | }, 219 | { 220 | "args": [ 221 | "-r", 222 | "-X", 223 | "../../debug.zip", 224 | "RealtekRTL8111.kext" 225 | ], 226 | "cwd": "build/Debug", 227 | "name": "Zip Debug Directory", 228 | "path": "zip" 229 | } 230 | ], 231 | "Release File": "release.zip", 232 | "URL": "https://github.com/Mieze/RTL8111_driver_for_OS_X" 233 | }, 234 | { 235 | "Debug File": "build/Debug/*.zip", 236 | "Desc": "Lilu kernel extension for blocking unwanted processes and unlocking support for certain features restricted to other hardware", 237 | "Lilu": true, 238 | "MacKernelSDK": true, 239 | "Name": "RestrictEvents", 240 | "Release File": "build/Release/*.zip", 241 | "URL": "https://github.com/acidanthera/RestrictEvents" 242 | }, 243 | { 244 | "Debug File": "build/Debug/*.zip", 245 | "Desc": "Serial mouse kernel extension for macOS", 246 | "MacKernelSDK": true, 247 | "Name": "SerialMouse", 248 | "Release File": "build/Release/*.zip", 249 | "URL": "https://github.com/Goldfish64/SerialMouse" 250 | }, 251 | { 252 | "Debug File": "build/Debug/*.zip", 253 | "Desc": "UEFI framebuffer driver for macOS", 254 | "Lilu": true, 255 | "MacKernelSDK": true, 256 | "Name": "UEFIGraphicsFB", 257 | "Release File": "build/Release/*.zip", 258 | "URL": "https://github.com/acidanthera/UEFIGraphicsFB" 259 | }, 260 | { 261 | "32-bit": true, 262 | "Build Opts": [ 263 | "-target", 264 | "Package" 265 | ], 266 | "Debug File": "build/Debug/*.zip", 267 | "Desc": "advanced Apple SMC emulator in the kernel", 268 | "Lilu": true, 269 | "MacKernelSDK": true, 270 | "Name": "VirtualSMC", 271 | "Release File": "build/Release/*.zip", 272 | "URL": "https://github.com/acidanthera/VirtualSMC" 273 | }, 274 | { 275 | "Build Dir": "build/Build/Products/", 276 | "Build Opts": [ 277 | "-workspace", 278 | "VoodooI2C.xcworkspace", 279 | "-scheme", 280 | "VoodooI2C" 281 | ], 282 | "Debug File": "build/Build/Products/Debug/debug.zip", 283 | "Desc": "Intel I2C controller and slave device drivers for macOS", 284 | "Extras": [ 285 | "build/Build/Products/Release/release-dSYM.zip" 286 | ], 287 | "MacKernelSDK": true, 288 | "Name": "VoodooI2C", 289 | "Post-Build": [ 290 | { 291 | "args": [ 292 | "-r", 293 | "-X", 294 | "release.zip", 295 | ".", 296 | "-i", 297 | "./*.kext/*" 298 | ], 299 | "cwd": "build/Build/Products/Release", 300 | "name": "Zip Release Directory", 301 | "path": "zip" 302 | }, 303 | { 304 | "args": [ 305 | "-r", 306 | "-X", 307 | "release-dSYM.zip", 308 | ".", 309 | "-i", 310 | "./*.dSYM/*" 311 | ], 312 | "cwd": "build/Build/Products/Release", 313 | "name": "Zip Release dSYM", 314 | "path": "zip" 315 | }, 316 | { 317 | "args": [ 318 | "-r", 319 | "-X", 320 | "debug.zip", 321 | ".", 322 | "-i", 323 | "./*.kext/*" 324 | ], 325 | "cwd": "build/Build/Products/Debug", 326 | "name": "Zip Debug Directory", 327 | "path": "zip" 328 | } 329 | ], 330 | "Pre-Build": [ 331 | { 332 | "args": [ 333 | "-LfsO", 334 | "https://raw.githubusercontent.com/acidanthera/VoodooInput/master/VoodooInput/Scripts/bootstrap.sh" 335 | ], 336 | "name": "Download VoodooInput Bootstrap Script", 337 | "path": "curl" 338 | }, 339 | { 340 | "args": [ 341 | "+x", 342 | "bootstrap.sh" 343 | ], 344 | "name": "Make Bootstrap Executable", 345 | "path": "chmod" 346 | }, 347 | { 348 | "args": [], 349 | "name": "Run VoodooInput Bootstrap", 350 | "path": "./bootstrap.sh" 351 | }, 352 | { 353 | "args": [ 354 | "VoodooInput", 355 | "Dependencies/" 356 | ], 357 | "name": "Move VoodooInput to Dependencies", 358 | "path": "mv" 359 | } 360 | ], 361 | "Release File": "build/Build/Products/Release/release.zip", 362 | "Type": "Kext", 363 | "URL": "https://github.com/VoodooI2C/VoodooI2C" 364 | }, 365 | { 366 | "Debug File": "build/Debug/*.zip", 367 | "Desc": "Generic Multitouch Handler kernel extension for macOS", 368 | "Lilu": true, 369 | "MacKernelSDK": true, 370 | "Name": "VoodooInput", 371 | "Release File": "build/Release/*.zip", 372 | "URL": "https://github.com/acidanthera/VoodooInput" 373 | }, 374 | { 375 | "Debug File": "build/Debug/*.zip", 376 | "Desc": "PS2 controller kext", 377 | "Info": "build/Release/VoodooPS2Controller.kext/Contents/Info.plist", 378 | "Lilu": true, 379 | "MacKernelSDK": true, 380 | "Name": "VoodooPS2", 381 | "Pre-Build": [ 382 | { 383 | "args": [ 384 | "-LfsO", 385 | "https://raw.githubusercontent.com/acidanthera/VoodooInput/master/VoodooInput/Scripts/bootstrap.sh" 386 | ], 387 | "name": "Download VoodooInput Bootstrap Script", 388 | "path": "curl" 389 | }, 390 | { 391 | "args": [ 392 | "+x", 393 | "bootstrap.sh" 394 | ], 395 | "name": "Make Bootstrap Executable", 396 | "path": "chmod" 397 | }, 398 | { 399 | "args": [], 400 | "name": "Run VoodooInput Bootstrap", 401 | "path": "./bootstrap.sh" 402 | } 403 | ], 404 | "Release File": "build/Release/*.zip", 405 | "URL": "https://github.com/acidanthera/VoodooPS2" 406 | }, 407 | { 408 | "Command": "make", 409 | "Debug File": "build/Debug/*.zip", 410 | "Desc": "Refined macOS driver for ALPS TouchPads", 411 | "Info": "VoodooPS2Controller.kext/Contents/Info.plist", 412 | "Name": "VoodooPS2-Alps", 413 | "Post-Build": [ 414 | { 415 | "args": [ 416 | "-r", 417 | "-X", 418 | "release.zip", 419 | "." 420 | ], 421 | "cwd": "build/Release", 422 | "name": "Zip Release Directory", 423 | "path": "zip" 424 | }, 425 | { 426 | "args": [ 427 | "-r", 428 | "-X", 429 | "debug.zip", 430 | "." 431 | ], 432 | "cwd": "build/Debug", 433 | "name": "Zip Debug Directory", 434 | "path": "zip" 435 | } 436 | ], 437 | "Release File": "build/Release/*.zip", 438 | "URL": "https://github.com/1Revenger1/VoodooPS2-Alps" 439 | }, 440 | { 441 | "Build Dir": "build/Build/Products/", 442 | "Build Opts": [ 443 | "-scheme", 444 | "VoodooRMI" 445 | ], 446 | "Debug File": "build/Build/Products/Debug/*.zip", 447 | "Desc": "Synaptic Trackpad driver over SMBus/I2C for macOS", 448 | "MacKernelSDK": true, 449 | "Name": "VoodooRMI", 450 | "Release File": "build/Build/Products/Release/*.zip", 451 | "Type": "Kext", 452 | "URL": "https://github.com/VoodooSMBus/VoodooRMI" 453 | }, 454 | { 455 | "Build Dir": "build/Build/Products/", 456 | "Build Opts": [ 457 | "-scheme", 458 | "VoodooSMBus" 459 | ], 460 | "Debug File": "build/Build/Products/Debug/debug.zip", 461 | "Desc": "i2c-i801 driver port for macOS X + ELAN SMBus macOS X driver for Thinkpad T480s, L380, P52", 462 | "Name": "VoodooSMBus", 463 | "Post-Build": [ 464 | { 465 | "args": [ 466 | "-r", 467 | "-X", 468 | "release.zip", 469 | ".", 470 | "-i", 471 | "./*.kext/*" 472 | ], 473 | "cwd": "build/Build/Products/Release", 474 | "name": "Zip Release Directory", 475 | "path": "zip" 476 | }, 477 | { 478 | "args": [ 479 | "-r", 480 | "-X", 481 | "debug.zip", 482 | ".", 483 | "-i", 484 | "./*.kext/*" 485 | ], 486 | "cwd": "build/Build/Products/Debug", 487 | "name": "Zip Debug Directory", 488 | "path": "zip" 489 | } 490 | ], 491 | "Release File": "build/Build/Products/Release/release.zip", 492 | "Type": "Kext", 493 | "URL": "https://github.com/VoodooSMBus/VoodooSMBus" 494 | }, 495 | { 496 | "Debug File": "build/Debug/*.zip", 497 | "Desc": "provides patches for AMD/Nvidia/Intel GPUs", 498 | "Lilu": true, 499 | "MacKernelSDK": true, 500 | "Name": "WhateverGreen", 501 | "Release File": "build/Release/*.zip", 502 | "URL": "https://github.com/acidanthera/WhateverGreen" 503 | }, 504 | { 505 | "Build Opts": [ 506 | "-arch", 507 | "x86_64", 508 | "-project", 509 | "gfxutil.xcodeproj", 510 | "ONLY_ACTIVE_ARCH=NO" 511 | ], 512 | "Debug File": "build/Debug/*.zip", 513 | "Desc": "OpenCore front end", 514 | "EDK II": true, 515 | "MacKernelSDK": true, 516 | "Name": "gfxutil", 517 | "Release File": "build/Release/*.zip", 518 | "Type": "Utility", 519 | "URL": "https://github.com/acidanthera/gfxutil", 520 | "Version": [ 521 | "awk", 522 | "/^#define VERSION/ { print substr($3,2,5) }", 523 | "main.h" 524 | ] 525 | } 526 | ] 527 | } 528 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Builder dependencies 2 | hammock 3 | python-dateutil 4 | datetime 5 | termcolor2 6 | purl 7 | python-magic 8 | humanize 9 | gitpython 10 | cryptography 11 | 12 | # For ACID32 13 | macholib -------------------------------------------------------------------------------- /sort_plugins.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import json 3 | 4 | plugins = json.load(Path("plugins.json").open()) 5 | plugins["Plugins"].sort(key=lambda x: x["Name"]) 6 | json.dump(plugins, Path("plugins.json").open("w"), indent=2, sort_keys=True) 7 | -------------------------------------------------------------------------------- /update_config.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | import os 4 | import sys 5 | import urllib.parse 6 | from pathlib import Path 7 | 8 | import dateutil.parser 9 | import git 10 | from hammock import Hammock as hammock 11 | 12 | from config_mgmt import save_config 13 | 14 | token = sys.argv[1].strip() 15 | 16 | 17 | config: dict = json.load(Path("Config/config.json").open()) 18 | plugins = json.load(Path("plugins.json").open()) 19 | 20 | # version 2 to 3 21 | 22 | if config["_version"] == 2: 23 | def add_author_date(name, version): 24 | if version.get("date_authored", None): 25 | return version 26 | else: 27 | organization = repo = None 28 | for plugin in plugins["Plugins"]: 29 | if name == "AppleSupportPkg" or name == "BT4LEContinuityFixup": 30 | repo = name 31 | organization = "acidanthera" 32 | break 33 | elif name == "NoTouchID": 34 | repo = name 35 | organization = "al3xtjames" 36 | break 37 | if plugin["Name"] == name: 38 | organization, repo = plugin["URL"].strip().replace("https://github.com/", "").split("/") 39 | break 40 | if not repo: 41 | print("Product " + name + " not found") 42 | raise Exception 43 | commit_date = dateutil.parser.parse( 44 | json.loads(hammock("https://api.github.com").repos(organization, repo).commits(version["commit"]["sha"]).GET(auth=("github-actions", token)).text)["commit"]["author"]["date"] 45 | ) 46 | version["date_authored"] = commit_date.isoformat() 47 | return version 48 | 49 | config = {i: v for i, v in config.items() if not i.startswith("_")} 50 | 51 | for i in config: 52 | for j, item in enumerate(config[i]["versions"]): 53 | config[i]["versions"][j] = add_author_date(i, item) 54 | print(f"Added {config[i]['versions'][j]['date_authored']} for {i} {config[i]['versions'][j]['commit']['sha']}") 55 | 56 | for i in config: 57 | for j, item in enumerate(config[i]["versions"]): 58 | if not config[i]["versions"][j].get("date_committed"): 59 | config[i]["versions"][j]["date_committed"] = config[i]["versions"][j].pop("datecommitted") 60 | if not config[i]["versions"][j].get("date_built"): 61 | config[i]["versions"][j]["date_built"] = config[i]["versions"][j].pop("dateadded") 62 | 63 | config[i]["versions"].sort(key=lambda x: (x["date_committed"], x["date_authored"]), reverse=True) 64 | 65 | config["_version"] = 3 66 | 67 | # version 3 to 4 68 | # nothing changed, but the other json files were added 69 | 70 | if config["_version"] == 3: 71 | config["_version"] = 4 72 | 73 | save_config(config) 74 | 75 | if os.environ.get("PROD", "false") == "true": 76 | repo = git.Repo("Config") 77 | if repo.is_dirty(untracked_files=True): 78 | repo.git.add(all=True) 79 | repo.git.commit(message="Deploying to builds") 80 | repo.git.push() 81 | -------------------------------------------------------------------------------- /updater.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import os 4 | import sys 5 | import traceback 6 | from pathlib import Path 7 | 8 | import dateutil.parser 9 | import git 10 | import humanize 11 | from hammock import Hammock as hammock 12 | from termcolor2 import c as color 13 | 14 | import builder 15 | from add import add_built 16 | from notify import notify_error, notify_failure, notify_success 17 | 18 | 19 | def matched_key_in_dict_array(array, key, value): 20 | if not array: 21 | return False 22 | for dictionary in array: 23 | if dictionary.get(key, None) == value: 24 | return True 25 | return False 26 | 27 | 28 | MAX_OUTSTANDING_COMMITS = 3 29 | DATE_DELTA = 7 30 | RETRIES_BEFORE_FAILURE = 2 31 | 32 | theJSON = json.load(Path("plugins.json").open()) 33 | plugins = theJSON.get("Plugins", []) 34 | 35 | config_dir = Path("Config").resolve() 36 | 37 | config = json.load((config_dir / Path("config.json")).open()) 38 | failures = json.load((config_dir / Path("failures.json")).open()) 39 | 40 | 41 | def add_to_failures(plugin): 42 | if not failures.get(plugin["plugin"]["Name"]): 43 | failures[plugin["plugin"]["Name"]] = {plugin["commit"]["sha"]: 1} 44 | elif not failures[plugin["plugin"]["Name"]].get(plugin["commit"]["sha"]): 45 | failures[plugin["plugin"]["Name"]][plugin["commit"]["sha"]] = 1 46 | else: 47 | failures[plugin["plugin"]["Name"]][plugin["commit"]["sha"]] += 1 48 | 49 | 50 | last_updated_path = config_dir / Path("last_updated.txt") 51 | 52 | info = [] 53 | to_build = [] 54 | to_add = [] 55 | 56 | if last_updated_path.is_file() and last_updated_path.stat().st_size != 0: 57 | date_to_compare = dateutil.parser.parse(last_updated_path.read_text()) 58 | last_updated_path.write_text(datetime.datetime.now(tz=datetime.timezone.utc).isoformat()) 59 | else: 60 | last_updated_path.touch() 61 | date_to_compare = datetime.datetime(2021, 3, 1, tzinfo=datetime.timezone.utc) 62 | last_updated_path.write_text(date_to_compare.isoformat()) 63 | 64 | print("Last update date is " + date_to_compare.isoformat()) 65 | 66 | token = sys.argv[1].strip() 67 | 68 | for plugin in plugins: 69 | organization, repo = plugin["URL"].strip().replace("https://github.com/", "").split("/") 70 | base_url = hammock("https://api.github.com") 71 | 72 | releases_url = base_url.repos(organization, repo).releases.GET(auth=("github-actions", token), params={"per_page": 100}) 73 | releases = json.loads(releases_url.text or releases_url.content) 74 | if releases_url.headers.get("Link"): 75 | print(releases_url.headers["Link"]) 76 | 77 | commits_url = base_url.repos(organization, repo).commits.GET(auth=("github-actions", token), params={"per_page": 100}) 78 | commits = json.loads(commits_url.text or commits_url.content) 79 | if releases_url.headers.get("Link"): 80 | print(releases_url.headers["Link"]) 81 | 82 | count = 1 83 | 84 | for commit in commits: 85 | commit_date = dateutil.parser.parse(commit["commit"]["committer"]["date"]) 86 | newer = commit_date >= date_to_compare - datetime.timedelta(days=DATE_DELTA) 87 | 88 | if isinstance(plugin.get("Force", None), str): 89 | force_build = commit["sha"] == plugin.get("Force") 90 | else: 91 | force_build = plugin.get("Force") and commits.index(commit) == 0 92 | 93 | not_in_repo = True 94 | for i in config.get(plugin["Name"], {}).get("versions", []): 95 | if i["commit"]["sha"] == commit["sha"]: 96 | not_in_repo = False 97 | 98 | hit_failure_threshold = failures.get(plugin["Name"], {}).get(commit["sha"], 0) > RETRIES_BEFORE_FAILURE 99 | within_max_outstanding = count <= plugin.get("Max Per Run", MAX_OUTSTANDING_COMMITS) 100 | 101 | # Do not build if we hit the limit for builds per run for this plugin. 102 | if not within_max_outstanding: 103 | continue 104 | 105 | # Build if: 106 | # Newer than last checked and not in repo, OR not in repo and latest commit 107 | # AND must not have hit failure threshold (retries >= RETRIES_BEFORE_FAILURE) 108 | # OR Force is set to true (ignores blacklist as this is manual intervention) 109 | 110 | if (((newer and not_in_repo) or (not_in_repo and commits.index(commit) == 0)) and not hit_failure_threshold) or force_build: 111 | if commits.index(commit) == 0: 112 | print(plugin["Name"] + " by " + organization + " latest commit (" + commit_date.isoformat() + ") not built") 113 | else: 114 | print(plugin["Name"] + " by " + organization + " commit " + commit["sha"] + " (" + commit_date.isoformat() + ") not built") 115 | to_build.append({"plugin": plugin, "commit": commit}) 116 | count += 1 117 | elif hit_failure_threshold: 118 | print(plugin["Name"] + " by " + organization + " commit " + commit["sha"] + " (" + commit_date.isoformat() + ") has hit failure threshold!") 119 | 120 | for release in releases: 121 | release_date = dateutil.parser.parse(release["created_at"]) 122 | if release_date >= date_to_compare: 123 | if releases.index(release) == 0: 124 | print(plugin["Name"] + " by " + organization + " latest release (" + release_date.isoformat() + ") not added") 125 | else: 126 | print(plugin["Name"] + " by " + organization + " release " + release["name"] + " (" + release_date.isoformat() + ") not added") 127 | to_add.append({"plugin": plugin, "release": release}) 128 | 129 | 130 | # for i in to_add: addRelease(i) 131 | 132 | 133 | # Start setting up builder here. 134 | builder = builder.Builder() 135 | 136 | failed = [] 137 | succeeded = [] 138 | errored = [] 139 | 140 | print(color(f"\nBuilding {len(to_build)} things").bold) 141 | for plugin in to_build: 142 | print(f"\nBuilding {color(plugin['plugin']['Name']).bold}") 143 | try: 144 | started = datetime.datetime.now() 145 | files = None 146 | files = builder.build(plugin["plugin"], commithash=plugin["commit"]["sha"]) 147 | except Exception as error: 148 | duration = datetime.datetime.now() - started 149 | 150 | print("An error occurred!") 151 | print(error) 152 | traceback.print_tb(error.__traceback__) 153 | if files: 154 | print(f"Files: {files}") 155 | 156 | print(f"{color('Building of').red} {color(plugin['plugin']['Name']).red.bold} {color('errored').red}") 157 | print(f"Took {humanize.naturaldelta(duration)}") 158 | notify_error(token, plugin) 159 | errored.append(plugin) 160 | add_to_failures(plugin) 161 | continue 162 | 163 | duration = datetime.datetime.now() - started 164 | 165 | if files: 166 | print(f"{color('Building of').green} {color(plugin['plugin']['Name']).green.bold} {color('succeeded').green}") 167 | print(f"Took {humanize.naturaldelta(duration)}") 168 | 169 | results = plugin 170 | results["files"] = files 171 | 172 | print("Adding to config...") 173 | results["config_item"] = add_built(results, token) 174 | notify_success(token, results) 175 | succeeded.append(results) 176 | else: 177 | print(f"{color('Building of').red} {color(plugin['plugin']['Name']).red.bold} {color('failed').red}") 178 | print(f"Took {humanize.naturaldelta(duration)}") 179 | 180 | notify_failure(token, plugin) 181 | failed.append(plugin) 182 | add_to_failures(plugin) 183 | 184 | print(color(f"\n{len(succeeded)} of {len(to_build)} built successfully\n").bold) 185 | if len(succeeded) > 0: 186 | print(color("Succeeded:").green) 187 | for i in succeeded: 188 | print(i["plugin"]["Name"]) 189 | if len(failed) > 0: 190 | print(color("\nFailed:").red) 191 | for i in failed: 192 | print(i["plugin"]["Name"]) 193 | if len(errored) > 0: 194 | print(color("\nErrored:").red) 195 | for i in errored: 196 | print(i["plugin"]["Name"]) 197 | 198 | json.dump(failures, (config_dir / Path("failures.json")).open("w"), indent=2, sort_keys=True) 199 | 200 | 201 | if os.environ.get("PROD", "false") == "true": 202 | repo = git.Repo(config_dir) 203 | if repo.is_dirty(untracked_files=True): 204 | repo.git.add(all=True) 205 | repo.git.commit(message="Deploying to builds") 206 | repo.git.push() 207 | 208 | 209 | if len(failed) > 0 or len(errored) > 0: 210 | sys.exit(10) 211 | --------------------------------------------------------------------------------