|
| 1 | +import os |
| 2 | +import sys |
| 3 | +import json |
| 4 | +import requests |
| 5 | +import shutil |
| 6 | +import markdownify |
| 7 | +from pathlib import Path |
| 8 | +from urllib.parse import urljoin |
| 9 | +from lxml import etree, html |
| 10 | +from github import Github |
| 11 | +from typing import Union |
| 12 | + |
| 13 | +# Useful: |
| 14 | +# - https://docs.blender.org/manual/en/latest/advanced/extensions/creating_repository/static_repository.html |
| 15 | +# - https://developer.blender.org/docs/features/extensions/api_listing/ |
| 16 | + |
| 17 | + |
| 18 | +PACKAGES_FOLDER = Path(".") |
| 19 | +INDEX_PATH = PACKAGES_FOLDER / "index.json" |
| 20 | +HTML_PATH = PACKAGES_FOLDER / "index.html" |
| 21 | +MD_PATH = PACKAGES_FOLDER / "readme.md" |
| 22 | +MD_HEADER_PATH = PACKAGES_FOLDER / "readme_header.md" |
| 23 | +INDEX_URL = "https://raw.githubusercontent.com/IfcOpenShell/blenderbim_unstable_repo/main/index.json" |
| 24 | +BASE_URL = "https://github.com/IfcOpenShell/IfcOpenShell/releases/download/blenderbim-{version}/" |
| 25 | +BLENDER_PLATFORMS = ["windows-x64", "macos-x64", "macos-arm64", "linux-x64"] |
| 26 | +# Blender doesn't support separate builds for different Python versions :( |
| 27 | +PYTHON_VERSION = "py311" |
| 28 | + |
| 29 | + |
| 30 | +def check_url(url) -> bool: |
| 31 | + try: |
| 32 | + response = requests.head(url, allow_redirects=True) # Use HEAD request to check URL status |
| 33 | + if response.status_code == 200: |
| 34 | + print(f"URL is reachable: {url}") |
| 35 | + return True |
| 36 | + else: |
| 37 | + print(f"URL returned status code {response.status_code}: {url}") |
| 38 | + except requests.RequestException as e: |
| 39 | + print(f"URL check failed with exception: {e}: {url}") |
| 40 | + |
| 41 | + return False |
| 42 | + |
| 43 | + |
| 44 | +def get_platform(filename: str) -> Union[str, None]: |
| 45 | + for platorm in BLENDER_PLATFORMS: |
| 46 | + if platorm in filename: |
| 47 | + return platorm |
| 48 | + |
| 49 | + |
| 50 | +class ExtensionsRepo: |
| 51 | + def __init__(self, github_tag: str): |
| 52 | + self.fetch_urls(github_tag) |
| 53 | + self.run_blender() |
| 54 | + self.patch_repo_files() |
| 55 | + |
| 56 | + def fetch_urls(self, github_tag: str) -> None: |
| 57 | + g = Github() |
| 58 | + repo = g.get_repo("IfcOpenShell/IfcOpenShell") |
| 59 | + |
| 60 | + if github_tag == "--last-tag": |
| 61 | + for i, release in enumerate(repo.get_releases()): |
| 62 | + if i >= 10: |
| 63 | + raise Exception("Couldn't find a release with a valid tag in the last 10 releases.") |
| 64 | + if release.tag_name.startswith("blenderbim-"): |
| 65 | + github_tag = release.tag_name |
| 66 | + break |
| 67 | + |
| 68 | + release = repo.get_release(github_tag) |
| 69 | + platforms_urls = {} |
| 70 | + for asset in release.get_assets(): |
| 71 | + name = asset.name |
| 72 | + if PYTHON_VERSION not in name: |
| 73 | + continue |
| 74 | + if not (platform := get_platform(name)): |
| 75 | + continue |
| 76 | + platforms_urls[platform] = asset.browser_download_url |
| 77 | + |
| 78 | + if len(platforms_urls) != len(BLENDER_PLATFORMS): |
| 79 | + missing_platforms = set(BLENDER_PLATFORMS) - set(platforms_urls) |
| 80 | + raise Exception( |
| 81 | + f"Couldn't find in the release '{github_tag}' .zip files for some platforms: '{missing_platforms}'." |
| 82 | + ) |
| 83 | + |
| 84 | + for url in platforms_urls.values(): |
| 85 | + print(f"Downloading {url}...") |
| 86 | + with requests.get(url, stream=True) as r, open(PACKAGES_FOLDER / url.rsplit("/", 1)[-1], "wb") as f: |
| 87 | + shutil.copyfileobj(r.raw, f) |
| 88 | + print("Finished downloading .zip packages.") |
| 89 | + |
| 90 | + def run_blender(self) -> None: |
| 91 | + return_code = os.system(f"blender --command extension server-generate --repo-dir={PACKAGES_FOLDER} --html") |
| 92 | + if return_code != 0: |
| 93 | + raise Exception(f"Blender return code was '{return_code}'.") |
| 94 | + |
| 95 | + print("Finished blender 'extension server-generate'.") |
| 96 | + |
| 97 | + def patch_repo_files(self) -> None: |
| 98 | + self.replaced_urls = {} |
| 99 | + self.patch_index_json() |
| 100 | + self.patch_index_html() |
| 101 | + self.convert_html_to_md() |
| 102 | + |
| 103 | + def patch_index_json(self) -> None: |
| 104 | + with open(INDEX_PATH, "rb") as fi: |
| 105 | + index = json.load(fi) |
| 106 | + |
| 107 | + replaced_urls = {} |
| 108 | + |
| 109 | + for package in index["data"]: |
| 110 | + version = package["version"] |
| 111 | + archive_url = package["archive_url"] |
| 112 | + url = urljoin(BASE_URL.format(version=version), archive_url) |
| 113 | + package["archive_url"] = url |
| 114 | + replaced_urls[archive_url] = url |
| 115 | + |
| 116 | + self.replaced_urls = replaced_urls |
| 117 | + |
| 118 | + with open(INDEX_PATH, "w") as fo: |
| 119 | + json.dump(index, fo, indent=2) |
| 120 | + fo.write("\n") |
| 121 | + |
| 122 | + print("Finished updating index.json") |
| 123 | + |
| 124 | + def patch_index_html(self) -> None: |
| 125 | + with open(HTML_PATH, "r", encoding="utf-8") as f: |
| 126 | + tree = html.parse(f) |
| 127 | + |
| 128 | + for a in tree.xpath("//a[starts-with(@href, './blenderbim_')]"): |
| 129 | + href = a.get("href") |
| 130 | + url, url_arguments = href.split("?") |
| 131 | + # Replace relative urls with absolute urls to the releases. |
| 132 | + # Replace relative index.json url to the repo url. |
| 133 | + url_arguments = url_arguments.replace(".%2Findex.json", INDEX_URL) |
| 134 | + new_href = f"{self.replaced_urls[url]}?{url_arguments}" |
| 135 | + a.set("href", new_href) |
| 136 | + |
| 137 | + with open(HTML_PATH, "w", encoding="utf-8") as f: |
| 138 | + html_string = etree.tostring(tree, method="html", pretty_print=True, encoding="utf-8").decode("utf-8") |
| 139 | + f.write(html_string) |
| 140 | + |
| 141 | + print("Finished updating index.html") |
| 142 | + |
| 143 | + def convert_html_to_md(self) -> None: |
| 144 | + with open(HTML_PATH, "r", encoding="utf-8") as file: |
| 145 | + html_content = file.read() |
| 146 | + |
| 147 | + parser = etree.HTMLParser() |
| 148 | + tree = etree.fromstring(html_content, parser) |
| 149 | + |
| 150 | + # Convert HTML to Markdown |
| 151 | + markdown_content = markdownify.markdownify( |
| 152 | + etree.tostring(tree, pretty_print=True, method="html").decode("utf-8") |
| 153 | + ) |
| 154 | + |
| 155 | + with open(MD_HEADER_PATH, "r", encoding="utf-8") as fo: |
| 156 | + with open(MD_PATH, "w", encoding="utf-8") as file: |
| 157 | + file.write(fo.read()) |
| 158 | + file.write(markdown_content) |
| 159 | + |
| 160 | + os.unlink(HTML_PATH) |
| 161 | + print("Finished updating readme.md") |
| 162 | + |
| 163 | + |
| 164 | +if __name__ == "__main__": |
| 165 | + if len(sys.argv) < 2: |
| 166 | + script_name = Path(__file__).name |
| 167 | + raise Exception(f"Usage: 'py {script_name} <github_releases_tag>' | 'py {script_name} --last-tag'") |
| 168 | + ExtensionsRepo(sys.argv[1]) |
0 commit comments