# # This file is licensed under the Affero General Public License (AGPL) version 3. # # Copyright 2022 The Matrix.org Foundation C.I.C. # Copyright (C) 2023 New Vector, Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # See the GNU Affero General Public License for more details: # . # # Originally licensed under the Apache License, Version 2.0: # . # # [This file includes modifications made by New Vector Limited] # # import json import os import urllib.parse from hashlib import blake2b from importlib.metadata import Distribution, PackageNotFoundError from typing import Optional import synapse from synapse.synapse_rust import get_rust_file_digest def check_rust_lib_up_to_date() -> None: """For editable installs check if the rust library is outdated and needs to be rebuilt. """ # Get the location of the editable install. synapse_root = get_synapse_source_directory() if synapse_root is None: return None # Get the hash of all Rust source files rust_path = os.path.join(synapse_root, "rust", "src") if not os.path.exists(rust_path): return None hash = _hash_rust_files_in_directory(rust_path) if hash != get_rust_file_digest(): raise Exception("Rust module outdated. Please rebuild using `poetry install`") def _hash_rust_files_in_directory(directory: str) -> str: """Get the hash of all files in a directory (recursively)""" directory = os.path.abspath(directory) paths = [] dirs = [directory] while dirs: dir = dirs.pop() with os.scandir(dir) as d: for entry in d: if entry.is_dir(): dirs.append(entry.path) else: paths.append(entry.path) # We sort to make sure that we get a consistent and well-defined ordering. paths.sort() hasher = blake2b() for path in paths: with open(os.path.join(directory, path), "rb") as f: hasher.update(f.read()) return hasher.hexdigest() def get_synapse_source_directory() -> Optional[str]: """Try and find the source directory of synapse for editable installs (like those used in development). Returns None if not an editable install (or otherwise can't find the source directory). """ # Try and find the installed matrix-synapse package. try: package = Distribution.from_name("matrix-synapse") except PackageNotFoundError: # The package is not found, so it's not installed and so must be being # pulled out from a local directory (usually the current one). synapse_dir = os.path.dirname(synapse.__file__) synapse_root = os.path.abspath(os.path.join(synapse_dir, "..")) # Double check we've not gone into site-packages... if os.path.basename(synapse_root) == "site-packages": return None # ... and it looks like the root of a python project. if not os.path.exists("pyproject.toml"): return None return synapse_root # Read the `direct_url.json` metadata for the package. This won't exist for # packages installed via a repository/etc. # c.f. https://packaging.python.org/en/latest/specifications/direct-url/ direct_url_json = package.read_text("direct_url.json") if direct_url_json is None: return None # c.f. https://packaging.python.org/en/latest/specifications/direct-url/ for # the format direct_url_dict: dict = json.loads(direct_url_json) # `url` must exist as a key, and point to where we fetched the repo from. project_url = urllib.parse.urlparse(direct_url_dict["url"]) # If its not a local file then we must have built the rust libs either a) # after we downloaded the package, or b) we built the download wheel. if project_url.scheme != "file": return None # And finally if its not an editable install then the files can't have # changed since we installed the package. if not direct_url_dict.get("dir_info", {}).get("editable", False): return None return project_url.path