#!/usr/bin/env python3 # # Copyright 2019-2021 Signal Messenger, LLC # SPDX-License-Identifier: AGPL-3.0-only # """ This script generates libringrtc.aar for distribution """ # ------------------------------------------------------------------------------ # # Imports # try: import argparse import enum import logging import subprocess import os import shutil except ImportError as e: raise ImportError(str(e) + '- required module not found') DEFAULT_ARCHS = ['arm', 'arm64', 'x86', 'x64'] NINJA_TARGETS = ['ringrtc'] JAR_FILES = [ 'lib.java/sdk/android/libwebrtc.jar', ] SO_LIBS = [ 'libringrtc_rffi.so', 'libringrtc.so', ] class Project(enum.Flag): WEBRTC = enum.auto() RINGRTC = enum.auto() AAR = enum.auto() ALL = WEBRTC | RINGRTC | AAR def __sub__(self, other): return self & ~other # ------------------------------------------------------------------------------ # # Main # def ParseArgs(): parser = argparse.ArgumentParser( description='Build and package libringrtc.aar') parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output') parser.add_argument('-q', '--quiet', action='store_true', help='Quiet output') parser.add_argument('--project-dir', default=os.path.dirname(os.path.dirname(os.path.abspath(__file__))), help='Project root directory') parser.add_argument('-b', '--build-dir', required=True, help='Build directory') parser.add_argument('-w', '--webrtc-src-dir', required=True, help='WebRTC source root directory') parser.add_argument('-o', '--output', default = 'libringrtc.aar', help='Output AAR file name') parser.add_argument('-d', '--debug-build', action='store_true', help='Build a debug version of the AAR. Default is both') parser.add_argument('-r', '--release-build', action='store_true', help='Build a release version of the AAR. Default is both') parser.add_argument('-a', '--arch', default=DEFAULT_ARCHS, choices=DEFAULT_ARCHS, nargs='*', help='CPU architectures to build. Defaults: %(default)s.') parser.add_argument('-g', '--extra-gn-args', nargs='*', default=[], help='''Additional GN arguments, passed via `gn --args` switch. These args override anything set internally by this script.''') parser.add_argument('-n', '--extra-ninja-flags', nargs='*', default=[], help='''Additional Ninja flags, overriding anything set internally by this script.''') parser.add_argument('-f', '--extra-gn-flags', nargs='*', default=[], help='''Additional GN flags, overriding anything set internally by this script.''') parser.add_argument('--extra-cargo-flags', nargs='*', default=[], help='Additional Cargo arguments') parser.add_argument('-j', '--jobs', default=32, help='Number of parallel ninja jobs to run.') parser.add_argument('--gradle-dir', required=True, help='Android gradle directory') parser.add_argument('--publish-version', required=True, help='Library version to publish') parser.add_argument('--extra-gradle-args', nargs='*', default=[], help='Additional gradle arguments') parser.add_argument('--install-local', action='store_true', help='Install to local maven repo') parser.add_argument('--install-dir', help='Install to local directory') parser.add_argument('--upload-sonatype-repo', help='Upload to remote sonatype repo') parser.add_argument('--upload-sonatype-user', help='Upload to remote sonatype repo as user') parser.add_argument('--upload-sonatype-password', help='Upload to remote sonatype repo using password') parser.add_argument('--signing-keyid', help='''GPG keyId for signing key (8 character short form). See https://docs.gradle.org/current/userguide/signing_plugin.html''') parser.add_argument('--signing-password', help='''GPG passphrase for signing key. See https://docs.gradle.org/current/userguide/signing_plugin.html''') parser.add_argument('--signing-secret-keyring', help='''Absolute path to the secret key ring file containing signing key. See https://docs.gradle.org/current/userguide/signing_plugin.html''') parser.add_argument('--dry-run', action='store_true', help='Dry Run: print what would happen, but do not actually do anything') parser.add_argument('-u', '--unstripped', action='store_true', help='Store the unstripped libraries in the .aar. Default is false') parser.add_argument('-c', '--compile-only', dest='disabled_projects', action='append_const', const=Project.AAR, help='Only compile the code, do not build the .aar. Default is false') parser.add_argument('--webrtc-only', dest='disabled_projects', action='append_const', const=Project.RINGRTC | Project.AAR, help='''Compile WebRTC's libraries only, then stop building''') parser.add_argument('--ringrtc-only', dest='disabled_projects', action='append_const', const=Project.WEBRTC, help='Compile RingRTC only, assuming WebRTC is already built') parser.add_argument('--clean', action='store_true', help='Remove all the build products. Default is false') return parser.parse_args() def RunSdkmanagerLicenses(dry_run): executable = os.path.join('third_party', 'android_sdk', 'public', 'cmdline-tools', 'latest', 'bin', 'sdkmanager') cmd = [ executable, '--licenses' ] logging.debug('Running: {}'.format(cmd)) if dry_run is False: subprocess.check_call(cmd) def RunCmd(dry_run, cmd): logging.debug('Running: {}'.format(cmd)) if dry_run is False: subprocess.check_call(cmd) def GetArchBuildRoot(build_dir, arch): return os.path.join(build_dir, 'android-{}'.format(arch)) def GetArchBuildDir(build_dir, arch, debug_build): if debug_build is True: build_type = 'debug' else: build_type = 'release' return os.path.join(GetArchBuildRoot(build_dir, arch), '{}'.format(build_type)) def GetOutputDir(build_dir, debug_build): if debug_build is True: build_type = 'debug' else: build_type = 'release' return os.path.join(build_dir, '{}'.format(build_type)) def GetGradleBuildDir(build_dir): return os.path.join(build_dir, 'gradle') def BuildArch(dry_run, project_dir, build_dir, arch, debug_build, extra_gn_args, extra_gn_flags, extra_ninja_flags, extra_cargo_flags, jobs, build_projects): logging.info('Building: {} ...'.format(arch)) output_dir = GetArchBuildDir(build_dir, arch, debug_build) if Project.WEBRTC in build_projects: gn_args = { 'target_os' : '"android"', 'target_cpu' : '"{}"'.format(arch), 'is_debug' : 'false', 'rtc_include_tests' : 'false', 'rtc_build_examples' : 'false', 'rtc_build_tools' : 'false', 'rtc_enable_protobuf' : 'false', 'rtc_enable_sctp' : 'false', 'rtc_libvpx_build_vp9': 'false', 'rtc_include_ilbc' : 'false', } if debug_build is True: gn_args['is_debug'] = 'true' gn_args['symbol_level'] = '2' gn_args_string = '--args=' + ' '.join( [k + '=' + v for k, v in gn_args.items()] + extra_gn_args) gn_total_args = [ 'gn', 'gen', output_dir, gn_args_string ] + extra_gn_flags RunCmd(dry_run, gn_total_args) ninja_args = [ 'ninja', '-C', output_dir ] + NINJA_TARGETS + [ '-j', jobs ] + extra_ninja_flags RunCmd(dry_run, ninja_args) if Project.RINGRTC in build_projects: # FIXME: Shouldn't hardcode Linux, but eventually this won't use WebRTC's NDK anyway. ndk_toolchain_dir = os.path.join( os.getcwd(), 'third_party', 'android_ndk', 'toolchains', 'llvm', 'prebuilt', 'linux-x86_64' ) cargo_args = [ 'cargo', 'rustc', '--target', GetCargoTarget(arch), '--target-dir', output_dir, '--manifest-path', os.path.join(project_dir, 'src', 'rust', 'Cargo.toml'), ] if not debug_build: cargo_args += ['--release'] cargo_args += extra_cargo_flags # Arguments directly for rustc cargo_args += [ '--', '-C', 'debuginfo=2', '-C', 'linker={}/bin/{}{}-clang'.format(ndk_toolchain_dir, GetClangTarget(arch), GetAndroidApiLevel(arch)), '-C', 'link-arg=-fuse-ld=lld', '-L', 'native=' + output_dir, ] RunCmd(dry_run, cargo_args) if dry_run: return # Copy the built library alongside libringrtc_rffi.so. shutil.copyfile( os.path.join(output_dir, GetCargoTarget(arch), 'debug' if debug_build else 'release', 'libringrtc.so'), os.path.join(output_dir, 'lib.unstripped', 'libringrtc.so')) # And strip another copy. strip_args = [ '{}/bin/llvm-strip'.format(ndk_toolchain_dir), '-s', os.path.join(output_dir, 'lib.unstripped', 'libringrtc.so'), '-o', os.path.join(output_dir, 'libringrtc.so'), ] RunCmd(dry_run, strip_args) def GetABI(arch): if arch == 'arm': return 'armeabi-v7a' elif arch == 'arm64': return 'arm64-v8a' elif arch == 'x86': return 'x86' elif arch == 'x64': return 'x86_64' else: raise Exception('Unknown architecture: ' + arch) def GetCargoTarget(arch): if arch == 'arm': return 'armv7-linux-androideabi' elif arch == 'arm64': return 'aarch64-linux-android' elif arch == 'x86': return 'i686-linux-android' elif arch == 'x64': return 'x86_64-linux-android' else: raise Exception('Unknown architecture: ' + arch) def GetClangTarget(arch): if arch == 'arm': return 'armv7a-linux-androideabi' else: return GetCargoTarget(arch) def GetAndroidApiLevel(arch): if arch == 'arm' or arch == 'x86': return 19 else: return 21 def CreateLibs(dry_run, project_dir, build_dir, archs, output, debug_build, unstripped, extra_gn_args, extra_gn_flags, extra_ninja_flags, extra_cargo_flags, jobs, build_projects): for arch in archs: BuildArch(dry_run, project_dir, build_dir, arch, debug_build, extra_gn_args, extra_gn_flags, extra_ninja_flags, extra_cargo_flags, jobs, build_projects) # The rest is considered part of the AAR build rather than the WebRTC or # RingRTC Rust builds mostly by process of elimination: sometimes we want # to do a "compile-only" build that skips assembling the libs/ directory. if Project.AAR not in build_projects: return output_dir = os.path.join(GetOutputDir(build_dir, debug_build), 'libs') output_file = os.path.join(output_dir, output) if dry_run is True: return shutil.rmtree(GetOutputDir(build_dir, debug_build), ignore_errors=True) os.makedirs(output_dir) for jar in JAR_FILES: logging.debug(' Adding jar: {} ...'.format(jar)) output_arch_dir = GetArchBuildDir(build_dir, archs[0], debug_build) shutil.copyfile(os.path.join(output_arch_dir, jar), os.path.join(output_dir, os.path.basename(jar))) for arch in archs: for lib in SO_LIBS: output_arch_dir = GetArchBuildDir(build_dir, arch, debug_build) if unstripped is True: # package the unstripped libraries lib_file = os.path.join('lib.unstripped', lib) else: lib_file = lib target_dir = os.path.join(output_dir, GetABI(arch)) logging.debug(' Adding lib: {}/{} to {}...'.format(GetABI(arch), lib_file, target_dir)) os.makedirs(target_dir, exist_ok=True) shutil.copyfile(os.path.join(output_arch_dir, lib_file), os.path.join(target_dir, os.path.basename(lib))) def RunGradle(dry_run, args): cmd = [ './gradlew' ] + args logging.debug('Running: {}'.format(cmd)) if dry_run is False: subprocess.check_call(cmd) def CreateAar(dry_run, extra_gradle_args, version, gradle_dir, sonatype_repo, sonatype_user, sonatype_password, signing_keyid, signing_password, signing_secret_keyring, build_projects, install_local, install_dir, project_dir, build_dir, archs, output, debug_build, release_build, unstripped, extra_gn_args, extra_gn_flags, extra_ninja_flags, extra_cargo_flags, jobs): build_types = [] if not (debug_build or release_build): # build both build_types = ['debug', 'release'] else: if debug_build: build_types = ['debug'] if release_build: build_types = build_types + ['release'] gradle_build_dir = GetGradleBuildDir(build_dir) shutil.rmtree(gradle_build_dir, ignore_errors=True) gradle_args = [ '-PringrtcVersion={}'.format(version), '-PbuildDir={}'.format(gradle_build_dir), ] if sonatype_repo is not None: sonatype_args = [ '-PsonatypeRepo={}'.format(sonatype_repo), '-PsignalSonatypeUsername={}'.format(sonatype_user), '-PsignalSonatypePassword={}'.format(sonatype_password), ] gradle_args.extend(sonatype_args) if signing_keyid is not None: gradle_args.append( '-Psigning.keyId={}'.format(signing_keyid)) if signing_password is not None: gradle_args.append( '-Psigning.password={}'.format(signing_password)) if signing_secret_keyring is not None: gradle_args.append( '-Psigning.secretKeyRingFile={}'.format(signing_secret_keyring)) for build_type in build_types: if build_type == 'debug': build_debug = True output_dir = GetOutputDir(build_dir, build_debug) lib_dir = os.path.join(output_dir, 'libs') gradle_args = gradle_args + [ "-PdebugRingrtcLibDir={}".format(lib_dir), "-PwebrtcJar={}/libwebrtc.jar".format(lib_dir), ] else: build_debug = False output_dir = GetOutputDir(build_dir, build_debug) lib_dir = os.path.join(output_dir, 'libs') gradle_args = gradle_args + [ "-PreleaseRingrtcLibDir={}".format(lib_dir), "-PwebrtcJar={}/libwebrtc.jar".format(lib_dir), ] CreateLibs(dry_run, project_dir, build_dir, archs, output, build_debug, unstripped, extra_gn_args, extra_gn_flags, extra_ninja_flags, extra_cargo_flags, jobs, build_projects) if Project.AAR not in build_projects: return gradle_args.extend(('assembleDebug' if build_type == 'debug' else 'assembleRelease' for build_type in build_types)) if install_local is True: if 'release' not in build_types: raise Exception('The `debug` build type is not supported with ' '--install-local. Remove --install-local and build again to ' 'have a debug AAR created in the Gradle output directory.') gradle_args.append('publishToMavenLocal') if sonatype_repo is not None: gradle_args.append(':publishMavenJavaPublicationToMavenRepository') gradle_args.extend(extra_gradle_args) # Run gradle os.chdir(os.path.abspath(gradle_dir)) RunGradle(dry_run, gradle_args) if install_dir is not None: for build_type in build_types: if build_type == 'debug': build_debug = True output_dir = GetOutputDir(build_dir, build_debug) dest_dir = os.path.join(install_dir, version, 'android', 'debug') else: build_debug = False output_dir = GetOutputDir(build_dir, build_debug) dest_dir = os.path.join(install_dir, version, 'android', 'release') logging.info('Installing locally to: {}'.format(dest_dir)) if dry_run is False: shutil.rmtree(dest_dir, ignore_errors=True) os.makedirs(os.path.dirname(dest_dir), exist_ok=True) shutil.copytree(output_dir, dest_dir) def clean_dir(directory, dry_run): logging.info('Removing: {}'.format(directory)) if dry_run is False: shutil.rmtree(directory, ignore_errors=True) def main(): args = ParseArgs() if args.dry_run is True: args.verbose = True if args.verbose is True: log_level = logging.DEBUG else: log_level = logging.INFO logging.basicConfig(level=log_level, format='%(levelname).1s:%(message)s') if args.quiet is True: logging.disable(logging.CRITICAL) build_dir = os.path.abspath(args.build_dir) logging.debug('Using build directory: {}'.format(build_dir)) if args.verbose is True: args.extra_ninja_flags = args.extra_ninja_flags + ['-v'] args.extra_cargo_flags = args.extra_cargo_flags + ['-v'] build_projects = Project.ALL for disabled_project in (args.disabled_projects or []): build_projects -= disabled_project gradle_dir = os.path.abspath(args.gradle_dir) logging.debug('Using gradle directory: {}'.format(gradle_dir)) if args.clean is True: for arch in DEFAULT_ARCHS: rm_dir = GetArchBuildRoot(build_dir, arch) clean_dir(GetArchBuildRoot(build_dir, arch), args.dry_run) clean_dir(GetGradleBuildDir(build_dir), args.dry_run) for dir in ('debug', 'release', 'javadoc', 'rustdoc', 'rust-lint'): clean_dir(os.path.join(build_dir, dir), args.dry_run) return 0 os.chdir(os.path.abspath(args.webrtc_src_dir)) RunSdkmanagerLicenses(args.dry_run) if args.upload_sonatype_repo is not None: if args.debug_build is True or args.release_build is True: print('ERROR: When uploading, must upload complete release and debug builds') print('ERROR: You cannot specify either --release or --debug while uploading') return 1 if args.upload_sonatype_user is None or args.upload_sonatype_password is None: print('ERROR: If --upload-sonatype-repo argument set, then both --upload-sonatype-user and --upload-sonatype-password must also be set.') return 1 if args.signing_keyid is None or \ args.signing_password is None or \ args.signing_secret_keyring is None: print('ERROR: If --upload-sonatype-repo argument set, then all of --signing-keyid, --signing-password, and --signing-secret-keyring must also be set.') return 1 CreateAar(args.dry_run, args.extra_gradle_args, args.publish_version, args.gradle_dir, args.upload_sonatype_repo, args.upload_sonatype_user, args.upload_sonatype_password, args.signing_keyid, args.signing_password, args.signing_secret_keyring, build_projects, args.install_local, args.install_dir, args.project_dir, build_dir, args.arch, args.output, args.debug_build, args.release_build, args.unstripped, args.extra_gn_args, args.extra_gn_flags, args.extra_ninja_flags, args.extra_cargo_flags, str(args.jobs)) logging.info(''' Version : {} Architectures : {} Debug Build : {} Release Build : {} Build Directory : {} Stripped Libraries: {} '''.format(args.publish_version, args.arch, args.debug_build, args.release_build, args.build_dir, not args.unstripped)) return 0 # -------------------- # # execution check # if __name__ == '__main__': exit(main())