#!/usr/bin/env python3 # License: Apache License v2 """Downloads the installed platforms of the selected SDK again when the CDN has a newer build of it. Run it as "tt update-sdk".""" import io import json import os import shutil import sys import urllib.request import zipfile CDN_URL = os.environ.get("TT_CDN", "https://cdn.tactilityproject.org") def fail(message): print(f"tt update-sdk: {message}", file=sys.stderr) sys.exit(1) def fetch(url): request = urllib.request.Request(url, headers={"User-Agent": "tt update-sdk"}) try: with urllib.request.urlopen(request, timeout=30) as response: return response.read() except OSError as error: fail(f"failed to download {url}: {error}") def read_commit(platform_directory): try: with open(os.path.join(platform_directory, "commit")) as file: return file.read().strip() except OSError: return None def installed_platforms(version_directory): prefix = "platform-" return sorted( name[len(prefix):] for name in os.listdir(version_directory) if name.startswith(prefix) and os.path.isdir(os.path.join(version_directory, name, "TactilitySDK")) ) def install_platform(platform_directory, archive_data, commit): """Extracts next to the installed SDK and then replaces it, refusing entries outside of it""" staging_directory = os.path.join(platform_directory, "TactilitySDK.new") shutil.rmtree(staging_directory, ignore_errors=True) os.makedirs(staging_directory) root = os.path.realpath(staging_directory) try: with zipfile.ZipFile(io.BytesIO(archive_data)) as archive: for member in archive.infolist(): destination = os.path.realpath(os.path.join(root, member.filename)) if not destination.startswith(root + os.sep): fail(f"invalid entry in the SDK archive: {member.filename}") archive.extractall(root) except zipfile.BadZipFile: shutil.rmtree(staging_directory, ignore_errors=True) fail("the downloaded SDK archive is invalid") sdk_directory = os.path.join(platform_directory, "TactilitySDK") shutil.rmtree(sdk_directory, ignore_errors=True) os.replace(staging_directory, sdk_directory) with open(os.path.join(platform_directory, "commit"), "w") as file: file.write(commit) def main(): version = os.environ.get("TACTILITY_SDK_VERSION") sdk_path = os.environ.get("TACTILITY_SDK_PATH") if version is None or sdk_path is None: fail("run this tool as 'tt update-sdk'") version_directory = os.path.join(sdk_path, "sdk", version) if os.environ.get("TT_LOCAL") == "1": fail("a local SDK can't be updated") index_data = fetch(f"{CDN_URL}/sdk/{version}/index.json") try: index = json.loads(index_data) except ValueError: fail("the downloaded index.json is invalid") commit = index.get("gitCommit", "") available = index.get("platforms", {}) updated = False for platform in installed_platforms(version_directory): platform_directory = os.path.join(version_directory, f"platform-{platform}") if read_commit(platform_directory) == commit or platform not in available: continue print(f"Updating SDK {version} for {platform}") install_platform(platform_directory, fetch(f"{CDN_URL}/sdk/{version}/{available[platform]}"), commit) updated = True with open(os.path.join(version_directory, "index.json"), "wb") as file: file.write(index_data) print(f"SDK {version} was updated" if updated else f"SDK {version} is up to date") if __name__ == "__main__": main()