diff --git a/.repackage-assets/maven_local.png b/.repackage-assets/maven_local.png new file mode 100644 index 0000000000..6624d4c995 Binary files /dev/null and b/.repackage-assets/maven_local.png differ diff --git a/repackage.md b/repackage.md new file mode 100644 index 0000000000..9d9a750a52 --- /dev/null +++ b/repackage.md @@ -0,0 +1,32 @@ +# Repackaging of the Video SDK + +Make sure you have `maven` and `python3` installed: +```bash +brew install maven +brew install python3 +``` + +Launch repackage script: +```bash +./repackage.sh --repackaged_webrtc --webrtc_android +``` + +When script completes, the usage of the new dependencies can be found in dogfooding's `build.gradle.kts`: +``` +// Stream Video SDK +implementation("io.getstream:streamx-video-android-compose:${Configuration.versionName}") +implementation("io.getstream:streamx-video-android-xml:${Configuration.versionName}") +implementation("io.getstream:streamx-video-android-tooling:${Configuration.versionName}") +implementation("io.getstream:streamx-video-android-datastore:${Configuration.versionName}") +compileOnly("io.getstream:streamx-video-android-mock:${Configuration.versionName}") +``` + +The repackaged modules are stored in: +``` +~/.m2/repository/io/getstream/streamx- +``` + +> **Note** +> `streamx` prefix means that it is a repackaged module and it's stored on your machine locally only. + +Stream Video for Android Header image \ No newline at end of file diff --git a/repackage.sh b/repackage.sh new file mode 100755 index 0000000000..a46ab3c4a6 --- /dev/null +++ b/repackage.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Parse named parameters +while [[ "$#" -gt 0 ]]; do + case $1 in + --repackaged_webrtc) repackaged_webrtc="$2"; shift ;; + --webrtc_android) webrtc_android="$2"; shift ;; + *) echo "Unknown parameter passed: $1"; exit 1 ;; + esac + shift +done + +# Run the Python command with the parameters +python3 scripts/repackage/repackage.py --repackaged_webrtc "$repackaged_webrtc" --webrtc_android "$webrtc_android" diff --git a/scripts/repackage/repackage.py b/scripts/repackage/repackage.py new file mode 100644 index 0000000000..6ca7e61d4c --- /dev/null +++ b/scripts/repackage/repackage.py @@ -0,0 +1,61 @@ +import argparse +import os + +from utils.project_configuration import extract_version_name_and_artifact_group +from utils.repackage_video_sdk import repackage_and_install_video_sdk +from utils.maven import install_file_to_local_maven + +from utils.repackage_webrtc_android import repackage_and_install_webrtc_android + + +def repackage_and_install( + repackaged_webrtc: str, + webrtc_android: str +): + script_root = os.getcwd() + if script_root.endswith('stream-video-android'): + script_root = os.path.join(script_root, 'scripts', 'repackage') + print(f'[REPACKAGE] root folder: "{script_root}"') + + # Install webrtc aar + os.chdir(webrtc_android) + configuration_path = os.path.join( + "buildSrc", "src", "main", "kotlin", "io", "getstream", "Configurations.kt" + ) + webrtc_android_version, group_id = extract_version_name_and_artifact_group(configuration_path) + install_file_to_local_maven( + file_path=repackaged_webrtc, + group_id=group_id, + artifact_id="streamx-webrtc-android", + version=webrtc_android_version, + packaging="aar" + ) + + # Repackage webrtc_android project + repackage_and_install_webrtc_android(webrtc_android) + + # Repackage video_sdk project + os.chdir(script_root) + video_sdk_path = os.path.dirname(os.path.dirname(script_root)) + repackage_and_install_video_sdk(video_sdk_path, webrtc_android_version) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Install Library B to local Maven repository.") + parser.add_argument('--repackaged_webrtc', required=True, + help='Path to AAR file with repackaged webrtc') + parser.add_argument('--webrtc_android', required=True, help='Path to webrtc-android repo') + + args = parser.parse_args() + + # Resolve relative paths + repackaged_webrtc = os.path.abspath(args.repackaged_webrtc) + webrtc_android = os.path.abspath(args.webrtc_android) + + print(f'[REPACKAGE] repackaged webrtc aar file: "{repackaged_webrtc}"') + print(f'[REPACKAGE] webrtc-android folder: "{webrtc_android}"') + + repackage_and_install( + repackaged_webrtc, + webrtc_android + ) diff --git a/scripts/repackage/utils/__init__.py b/scripts/repackage/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/repackage/utils/gradle_publish.py b/scripts/repackage/utils/gradle_publish.py new file mode 100644 index 0000000000..42aab4c335 --- /dev/null +++ b/scripts/repackage/utils/gradle_publish.py @@ -0,0 +1,26 @@ +def override_gradle_publish(file_path): + content = """ +apply plugin: 'maven-publish' + +group = PUBLISH_GROUP_ID +version = PUBLISH_VERSION + +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + groupId PUBLISH_GROUP_ID + artifactId PUBLISH_ARTIFACT_ID + version PUBLISH_VERSION + if (project.plugins.findPlugin("com.android.library")) { + from components.release + } else { + from components.java + } + } + } + } +} +""" + with open(file_path, 'w') as file: + file.write(content) diff --git a/scripts/repackage/utils/gradle_settings.py b/scripts/repackage/utils/gradle_settings.py new file mode 100644 index 0000000000..6028c55223 --- /dev/null +++ b/scripts/repackage/utils/gradle_settings.py @@ -0,0 +1,22 @@ +def modify_gradle_settings(): + file_path = 'settings.gradle.kts' + + # Read the content of the file + with open(file_path, 'r') as f: + lines = f.readlines() + + # Add mavenLocal() at appropriate places + for i, line in enumerate(lines): + if line.strip() == "repositories {": + space_count = len(line) - len(line.lstrip()) + indent = ' ' * space_count * 2 + lines.insert(i + 1, f"{indent}mavenLocal()\n") + + # Remove the line include(":stream-webrtc-android") + lines = [line for line in lines if 'include(":stream-webrtc-android")' not in line] + + # Write the modified content back to the original file + with open(file_path, 'w') as f: + f.writelines(lines) + + print(f"...{file_path} has been modified.") \ No newline at end of file diff --git a/scripts/repackage/utils/maven.py b/scripts/repackage/utils/maven.py new file mode 100644 index 0000000000..62d21c558e --- /dev/null +++ b/scripts/repackage/utils/maven.py @@ -0,0 +1,49 @@ +import os +import subprocess +import sys +from typing import Optional + + +def install_file_to_local_maven( + file_path: str, + group_id: str, + artifact_id: str, + version: str, + packaging: str, + sources: Optional[str] = None, + javadoc: Optional[str] = None, +): + if not os.path.exists(file_path): + sys.exit(f"Error: Path '{file_path}' does not exist.") + + # Install Library to the local Maven repository + mvn_cmd = [ + f"mvn", "install:install-file", + f"-Dfile={file_path}", + f"-DgroupId={group_id}", + f"-DartifactId={artifact_id}", + f"-Dversion={version}", + f"-Dpackaging={packaging}", + ] + + if sources and os.path.exists(sources): + mvn_cmd.extend([f"-Dsources={sources}"]) + + if javadoc and os.path.exists(javadoc): + mvn_cmd.extend([f"-Djavadoc={javadoc}"]) + + subprocess.run(mvn_cmd, check=True) + + # Output for verification + print(f"...installed library {artifact_id} version {version} to local Maven repository.") + + +def install_android_lib_module_to_local_maven(module_name: str): + # Clean module + subprocess.run(["./gradlew", f":{module_name}:clean"], check=True) + + # Assemble release AAR + subprocess.run(["./gradlew", f":{module_name}:assembleRelease"], check=True) + + # Publish to local maven + subprocess.run(["./gradlew", f":{module_name}:publishToMavenLocal"], check=True) diff --git a/scripts/repackage/utils/project_configuration.py b/scripts/repackage/utils/project_configuration.py new file mode 100644 index 0000000000..8d6b697c57 --- /dev/null +++ b/scripts/repackage/utils/project_configuration.py @@ -0,0 +1,32 @@ +import re + + +def extract_version_name_and_artifact_group(configuration_path): + with open(configuration_path, 'r') as file: + content = file.read() + + # Extract raw versionName string + version_raw_match = re.search(r'const val versionName = "(.*?)"', content) + version_raw = version_raw_match.group(1) if version_raw_match else None + + # Extract version components + major_version_match = re.search(r'const val majorVersion = (\d+)', content) + major_version = major_version_match.group(1) if major_version_match else None + + minor_version_match = re.search(r'const val minorVersion = (\d+)', content) + minor_version = minor_version_match.group(1) if minor_version_match else None + + patch_version_match = re.search(r'const val patchVersion = (\d+)', content) + patch_version = patch_version_match.group(1) if patch_version_match else None + + # Resolve versionName + version_name = version_raw\ + .replace("$majorVersion", major_version)\ + .replace("$minorVersion", minor_version)\ + .replace("$patchVersion", patch_version) + + # Extract artifactGroup + group_match = re.search(r'const val artifactGroup = "(.*?)"', content) + artifact_group = group_match.group(1) if group_match else None + + return version_name, artifact_group \ No newline at end of file diff --git a/scripts/repackage/utils/repackage_video_sdk.py b/scripts/repackage/utils/repackage_video_sdk.py new file mode 100644 index 0000000000..2f6d266119 --- /dev/null +++ b/scripts/repackage/utils/repackage_video_sdk.py @@ -0,0 +1,137 @@ +import os +import re +from datetime import datetime + +from utils.gradle_publish import override_gradle_publish +from utils.gradle_settings import modify_gradle_settings +from utils.maven import install_android_lib_module_to_local_maven +from utils.project_configuration import extract_version_name_and_artifact_group +from utils.string_replacement import replace_string_in_directory + + +def repackage_and_install_video_sdk(project_root: str, repackaged_webrtc_version: str): + start_time = int(datetime.utcnow().timestamp() * 1000) + os.chdir(project_root) + + configuration_path = os.path.join( + "buildSrc", "src", "main", "kotlin", "io", "getstream", "video", "android", + "Configuration.kt" + ) + project_version, group_id = extract_version_name_and_artifact_group(configuration_path) + print(f"> VideoSDK: version => {project_version}") + print(f"> VideoSDK: groupId => {group_id}") + + # Repackage + print(f"> VideoSDK: Repackage Started") + replace_string_in_directory( + directory_path=project_root, + search_string="org.webrtc", + replace_string="io.getstream.webrtc" + ) + print(f"> VideoSDK: Repackage Completed") + + # Modify settings.gradle file + os.chdir(project_root) + modify_gradle_settings() + print(f"> VideoSDK: settings.gradle has been modified") + + # Modify publish-module.gradle file + os.chdir(project_root) + override_gradle_publish(os.path.join('scripts', 'publish-module.gradle')) + print("> VideoSDK: publish-module.gradle has been modified") + + # Modify libs.versions.toml file + os.chdir(project_root) + _modify_gradle_libs_version(repackaged_webrtc_version) + + # Modify build.gradle files + os.chdir(project_root) + _modify_build_gradle_files(project_root) + + # Install modules + os.chdir(project_root) + _install_modules() + + # Updates dependencies in "dogfooding" + os.chdir(project_root) + _update_dogfooding_dependencies(project_root, group_id) + + now = int(datetime.utcnow().timestamp() * 1000) + elapsed = now - start_time + print(f"\nREPACKAGE SUCCESSFUL (VideoSDK) in {elapsed}ms") + + +def _modify_gradle_libs_version(repackaged_webrtc_version: str): + file_path = os.path.join("gradle", "libs.versions.toml") + + # streamWebRTC = "1.1.0" + with open(file_path, 'r') as file: + lines = file.readlines() + + with open(file_path, 'w') as file: + for line in lines: + if line.startswith("streamWebRTC"): + line = f'streamWebRTC = "{repackaged_webrtc_version}"\n' + elif "stream-webrtc-android" in line: + line = line.replace("stream-webrtc-android", "streamx-webrtc-android") + file.write(line) + print(f"...{file_path} has been modified.") + + +def _modify_build_gradle_files(folder: str) -> None: + for subdir, _, files in os.walk(folder): + for filename in files: + if 'build.gradle' in filename: + file_path = os.path.join(subdir, filename) + _modify_build_gradle(file_path) + + +def _modify_build_gradle(file_path: str) -> None: + # Read the content of the file + with open(file_path, 'r') as file: + lines = file.readlines() + + with open(file_path, 'w') as file: + for line in lines: + if "PUBLISH_ARTIFACT_ID" in line: + line = line.replace("stream-video-android", "streamx-video-android") + file.write(line) + + print(f"...{file_path} has been modified.") + + +def _install_modules(): + modules = [ + "stream-video-android-mock", + "stream-video-android-model", + "stream-video-android-datastore", + "stream-video-android-tooling", + "stream-video-android-core", + "stream-video-android-ui-common", + "stream-video-android-compose", + "stream-video-android-xml", + ] + for module in modules: + install_android_lib_module_to_local_maven(module) + + +def _update_dogfooding_dependencies(project_root: str, group_id: str) -> None: + # Read the content of the file + + file_path = os.path.join(project_root, "dogfooding", "build.gradle.kts") + with open(file_path, 'r') as file: + lines = file.readlines() + + with open(file_path, 'w') as file: + for line in lines: + if 'implementation(project(":stream-video-android-' in line: + match = re.search(r'stream-video-android-(\w+)', line) + dep_id = match.group(1) + line = f' implementation("{group_id}:streamx-video-android-{dep_id}:${{Configuration.versionName}}")\n' + if 'compileOnly(project(":stream-video-android-' in line: + match = re.search(r'stream-video-android-(\w+)', line) + dep_id = match.group(1) + line = f' compileOnly("{group_id}:streamx-video-android-{dep_id}:${{Configuration.versionName}}")\n' + file.write(line) + + print(f"...{file_path} has been modified.") \ No newline at end of file diff --git a/scripts/repackage/utils/repackage_webrtc_android.py b/scripts/repackage/utils/repackage_webrtc_android.py new file mode 100644 index 0000000000..2ded1a2b2b --- /dev/null +++ b/scripts/repackage/utils/repackage_webrtc_android.py @@ -0,0 +1,156 @@ +import os +import shutil +from datetime import datetime, time + +from utils.gradle_publish import override_gradle_publish +from utils.gradle_settings import modify_gradle_settings +from utils.maven import install_android_lib_module_to_local_maven +from utils.project_configuration import extract_version_name_and_artifact_group +from utils.string_replacement import replace_string_in_directory + + +def repackage_and_install_webrtc_android(path: str) -> str: + start_time = int(datetime.utcnow().timestamp() * 1000) + os.chdir(path) + + configuration_path = os.path.join( + "buildSrc", "src", "main", "kotlin", "io", "getstream", "Configurations.kt" + ) + project_version, group_id = extract_version_name_and_artifact_group(configuration_path) + print(f"> WebRTC-Android: version => {project_version}") + print(f"> WebRTC-Android: groupId => {group_id}") + + # Copy WebRTCException to `stream-webrtc-android-ktx` + file_name = 'WebRTCException.kt' + file_web_rtc_exception = os.path.join('stream-webrtc-android-utils', file_name) + target_dir = os.path.join('stream-webrtc-android-ktx', 'src', 'main', 'kotlin', 'io', + 'getstream', 'webrtc') + shutil.copy(file_web_rtc_exception, target_dir) + _make_kotlin_class_public(os.path.join(target_dir, file_name)) + + # Repackage + print("> WebRTC-Android: Repackage Started") + replace_string_in_directory( + directory_path=path, + search_string="org.webrtc", + replace_string="io.getstream.webrtc" + ) + print("> WebRTC-Android: Repackage Completed") + + # Modify settings.gradle file + os.chdir(path) + modify_gradle_settings() + print("> WebRTC-Android: settings.gradle has been modified") + + # Modify publish-module.gradle file + os.chdir(path) + override_gradle_publish(os.path.join('scripts', 'publish-module.gradle')) + print("> WebRTC-Android: publish-module.gradle has been modified") + + # Modify build.gradle files + print("> WebRTC-Android: modify build.gradle files") + _modify_build_gradle_files(path, project_version) + print("> WebRTC-Android: build.gradle files have been modified") + + # Install modules + os.chdir(path) + print(f"> WebRTC-Android: install modules") + _install_modules() + print("> WebRTC-Android: modules have been installed") + + now = int(datetime.utcnow().timestamp() * 1000) + elapsed = now - start_time + print(f"\nREPACKAGE SUCCESSFUL (WebRTC-Android) in {elapsed}ms") + return project_version + + +def _make_kotlin_class_public(file_path: str) -> None: + """ + Make the class declaration in a file explicitly public. + """ + with open(file_path, 'r', encoding='utf-8') as file: + content = file.readlines() + + modified_content = [] + for line in content: + stripped_line = line.strip() + if stripped_line.startswith("class ") and "public class" not in stripped_line: + indent = line[:line.index("class")] + modified_content.append(indent + "public " + line.lstrip()) + else: + modified_content.append(line) + + with open(file_path, 'w', encoding='utf-8') as file: + file.writelines(modified_content) + + +def _modify_build_gradle_files(path: str, project_version: str) -> None: + os.chdir(path) + for subdir, _, files in os.walk(path): + for filename in files: + if 'build.gradle' in filename: + file_path = os.path.join(subdir, filename) + _modify_build_gradle(file_path, project_version) + + +def _modify_build_gradle(file_path: str, project_version: str) -> None: + # Read the content of the file + with open(file_path, 'r') as file: + lines = file.readlines() + + with open(file_path, 'w') as file: + for line in lines: + if "PUBLISH_ARTIFACT_ID" in line: + line = line.replace("stream-webrtc-android", "streamx-webrtc-android") + elif 'api(project(":stream-webrtc-android"))' in line: + line = line.replace( + 'api(project(":stream-webrtc-android"))', + f'api("io.getstream:streamx-webrtc-android:{project_version}")' + ) + elif 'implementation(project(":stream-webrtc-android"))' in line: + line = line.replace( + 'implementation(project(":stream-webrtc-android"))', + f'implementation("io.getstream:streamx-webrtc-android:{project_version}")' + ) + file.write(line) + + print(f"...{file_path} has been modified.") + + +def _modify_build_gradle_webrtc_android_module( + module_name: str, + repackaged_module_name: str, + repackaged_webrtc_version: str, +): + file_path = os.path.join(module_name, "build.gradle.kts") + + # Read the content of the file + with open(file_path, 'r') as f: + content = f.read() + + # Perform replacements + content = content.replace( + f'api(project(":stream-webrtc-android"))', + f'api("io.getstream:streamx-webrtc-android:{repackaged_webrtc_version}")' + ) + content = content.replace( + f'implementation(project(":stream-webrtc-android"))', + f'implementation("io.getstream:streamx-webrtc-android:{repackaged_webrtc_version}")' + ) + content = content.replace(module_name, repackaged_module_name) + + # Write the modified content back to the original file + with open(file_path, 'w') as f: + f.write(content) + + print(f"...build.gradle.kts in {file_path} has been modified.") + + +def _install_modules(): + modules = [ + 'stream-webrtc-android-ui', + 'stream-webrtc-android-ktx', + 'stream-webrtc-android-compose' + ] + for module in modules: + install_android_lib_module_to_local_maven(module) diff --git a/scripts/repackage/utils/string_replacement.py b/scripts/repackage/utils/string_replacement.py new file mode 100644 index 0000000000..41b908fd15 --- /dev/null +++ b/scripts/repackage/utils/string_replacement.py @@ -0,0 +1,35 @@ +import os +from typing import Optional, List + + +def replace_string_in_directory(directory_path: str, search_string: str, replace_string: str, extensions: Optional[List[str]] = None) -> None: + """ + Recursively replace all occurrences of `search_string` with `replace_string` in all files within the directory + that have the specified extensions. + """ + if extensions is None: + extensions = ['.txt', '.md', '.kt', '.java', '.xml'] # default extensions + + for subdir, _, files in os.walk(directory_path): + for filename in files: + if filename.endswith(tuple(extensions)): + file_path = os.path.join(subdir, filename) + _replace_string_in_file(file_path, search_string, replace_string) + + +def _replace_string_in_file(file_path: str, search_string: str, replace_string: str) -> None: + """ + Replace all occurrences of `search_string` with `replace_string` in the specified file. + """ + try: + with open(file_path, 'r', encoding='utf-8') as file: + content = file.read() + except UnicodeDecodeError: + print(f"[ERROR] Failed to read {file_path} as UTF-8. Skipping...") + return + + new_content = content.replace(search_string, replace_string) + + if content != new_content: # Write new content only if there's a change + with open(file_path, 'w', encoding='utf-8') as file: + file.write(new_content)