#!/usr/bin/env python # Copyright 2020 Google # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Standalone script to build desktops apps with Firebase. Standalone self-sufficient (no external dependencies) python script that can ease building desktop apps with Firebase, by either using the C++ source (firebase-cpp-sdk github repo) or the prebuilt release Firebase libraries. Note that this script works with only Python3 (3.6+). Also note, that since this script runs a cmake configure step, it is advised to run it with a fresh clean build directory. Known side effects: If building against Firebase cpp source, this script might checkout a specific branch on github containing vcpkg. This will not be required once vcpkg is in the main branch. Example usage: Let's say we want to build the quickstart cpp example for Firebase database. As specified above, there are 2 options - build against the Firebase source or prebuilt Firebase libraries. # Build against the Firebase cpp sdk source python3 scripts/build_desktop_app_with_firebase.py --app_dir ~/quickstart-cpp/database/testapp --sdk_dir . --build_dir build_source (or) # Build against the prebuilt released Firebase libraries python3 scripts/build_desktop_app_with_firebase.py --app_dir ~/quickstart-cpp/database/testapp --sdk_dir ~/prebuilt/firebase_cpp_sdk_6.15.1/ --build_dir build_prebuilt # If the script ran successfully, it will print the path to the build directory. # The output looks like the following, Build successful! Please find your executables in build directory: /Users//quickstart-cpp/database/testapp/build_source # Running the built example $ ./Users//quickstart-cpp/database/testapp/build_source/desktop_testapp """ import argparse import os import platform import subprocess import sys def is_path_valid_for_cmake(path): """Check if specified path is setup for cmake.""" return os.path.exists(os.path.join(path, 'CMakeLists.txt')) def is_sdk_path_source(sdk_dir): """Validate if Firebase sdk dir is Firebase cpp source dir.""" # Not the most reliable way to detect if the sdk path is source or prebuilt # but should work for our purpose. return os.path.exists(os.path.join(sdk_dir, 'build_tools')) def validate_prebuilt_args(arch, config): """Validate cmd line args for build with prebuilt libraries.""" # Some options are not available when using prebuilt libraries. if platform.system() == 'Darwin': if arch == 'x86' or config == 'Debug': raise ValueError('Prebuilt mac Firebase libraries are built for x64 and ' 'Release mode only. ' 'Please fix the command line arguments and try again') if platform.system() == 'Linux': if config == 'Debug': raise ValueError('Prebuilt linux Firebase libraries are built with ' 'Release mode only. Please fix the --config command ' 'line argument and try again.') def get_vcpkg_triplet(arch, msvc_runtime_library='static'): """Get vcpkg target triplet (platform definition). Args: arch (str): Architecture (eg: 'x86', 'x64'). msvc_runtime_library (str): Runtime library for MSVC. (eg: 'static', 'dynamic'). Raises: ValueError: If current OS is not win, mac or linux. Returns: (str): Triplet name. Eg: "x64-windows-static". """ triplet_name = [arch] if platform.system() == 'Windows': triplet_name.append('windows') triplet_name.append('static') if msvc_runtime_library == 'dynamic': triplet_name.append('md') elif platform.system() == 'Darwin': triplet_name.append('osx') elif platform.system() == 'Linux': triplet_name.append('linux') else: raise ValueError('Unsupported platform. This function works only on ' 'Windows, Linux or Mac platforms.') triplet_name = '-'.join(triplet_name) print('Using vcpkg triplet: {0}'.format(triplet_name)) return triplet_name def build_source_vcpkg_dependencies(sdk_source_dir, arch, msvc_runtime_library): """Build C++ dependencies for Firebase source SDK using vcpkg. Args: sdk_source_dir (str): Path to Firebase C++ source directory. arch (str): Platform Architecture (eg: 'x64'). msvc_runtime_library (str): Runtime library for MSVC. (eg: 'static', 'dynamic'). """ # TODO(b/174141707): Remove this once dev branch of firebase-cpp-sdk repo has # been merged onto main branch. This is required because vcpkg lives only in # dev branch currently. subprocess.run(['git', 'checkout', 'dev'], cwd=sdk_source_dir, check=True) subprocess.run(['git', 'pull'], cwd=sdk_source_dir, check=True) # sys.executable should point to the python bin running this script. We use # the same executable to execute these subprocess python scripts. subprocess.run([sys.executable, 'scripts/gha/install_prereqs_desktop.py'], cwd=sdk_source_dir, check=True) subprocess.run([sys.executable, 'scripts/gha/build_desktop.py', '--arch', arch, '--msvc_runtime_library', msvc_runtime_library, '--vcpkg_step_only'], cwd=sdk_source_dir, check=True) def build_app_with_source(app_dir, sdk_source_dir, build_dir, arch, msvc_runtime_library='static', config=None, target_format=None): """Build desktop app directly against the Firebase C++ SDK source. Since this involves a cmake configure, it is advised to run this on a clean build directory. Args: app_dir (str): Path to directory containing application's CMakeLists.txt. sdk_source_dir (str): Path to Firebase C++ SDK source directory. (root of firebase-cpp-sdk github repo). build_dir (str): Output build directory. arch (str): Platform Architecture (example: 'x64'). msvc_runtime_library (str): Runtime library for MSVC. (eg: 'static', 'dynamic'). config (str): Release/Debug config. If it's not specified, cmake's default is used (most likely Debug). target_format (str): If specified, build for this target format. ('frameworks' or 'libraries'). """ # Cmake configure. cmd = ['cmake', '-S', '.', '-B', build_dir] cmd.append('-DFIREBASE_CPP_SDK_DIR={0}'.format(sdk_source_dir)) # If generator is not specified, default for platform is used by cmake, else # use the specified value. if config: cmd.append('-DCMAKE_BUILD_TYPE={0}'.format(config)) # workaround, absl doesn't build without tests enabled. cmd.append('-DBUILD_TESTING=off') if platform.system() == 'Linux' and arch == 'x86': # Use a separate cmake toolchain for cross compiling linux x86 builds. vcpkg_toolchain_file_path = os.path.join(sdk_source_dir, 'external', 'vcpkg', 'scripts', 'buildsystems', 'linux_32.cmake') else: vcpkg_toolchain_file_path = os.path.join(sdk_source_dir, 'external', 'vcpkg', 'scripts', 'buildsystems', 'vcpkg.cmake') cmd.append('-DCMAKE_TOOLCHAIN_FILE={0}'.format(vcpkg_toolchain_file_path)) vcpkg_triplet = get_vcpkg_triplet(arch, msvc_runtime_library) cmd.append('-DVCPKG_TARGET_TRIPLET={0}'.format(vcpkg_triplet)) if platform.system() == 'Windows': # If building for x86, we should supply -A Win32 to cmake configure. # Since the default architecture for cmake varies from machine to machine, # it is a good practice to specify it all the time (even for x64). cmd.append('-A') windows_cmake_arch_flag_value = 'Win32' if arch == 'x86' else 'x64' cmd.append(windows_cmake_arch_flag_value) # Use our special cmake option to specify /MD (dynamic) vs /MT (static). if msvc_runtime_library == 'static': cmd.append('-DMSVC_RUNTIME_LIBRARY_STATIC=ON') if target_format: cmd.append('-DFIREBASE_XCODE_TARGET_FORMAT={0}'.format(target_format)) print('Running {0}'.format(' '.join(cmd))) subprocess.run(cmd, cwd=app_dir, check=True) # CMake build. num_cpus = str(os.cpu_count()) cmd = ['cmake', '--build', build_dir, '-j', num_cpus, '--config', config] print('Running {0}'.format(' '.join(cmd))) subprocess.run(cmd, cwd=app_dir, check=True) def build_app_with_prebuilt(app_dir, sdk_prebuilt_dir, build_dir, arch, msvc_runtime_library='static', config=None): """Build desktop app directly against the prebuilt Firebase C++ libraries. Since this involves a cmake configure, it is advised to run this on a clean build directory. Args: app_dir (str): Path to directory containing application's CMakeLists.txt. sdk_prebuilt_dir (str): Path to prebuilt Firebase C++ libraries. build_dir (str): Output build directory. arch (str): Platform Architecture (eg: 'x64'). msvc_runtime_library (str): Runtime library for MSVC. (eg: 'static', 'dynamic'). config (str): Release/Debug config (eg: 'Release', 'Debug') If it's not specified, cmake's default is used (most likely Debug). """ cmd = ['cmake', '-S', '.', '-B', build_dir] cmd.append('-DFIREBASE_CPP_SDK_DIR={0}'.format(sdk_prebuilt_dir)) if platform.system() == 'Windows': if arch == 'x64': cmd.append('-DCMAKE_CL_64=ON') if msvc_runtime_library == 'dynamic': cmd.append('-DMSVC_RUNTIME_MODE=MD') else: cmd.append('-DMSVC_RUNTIME_MODE=MT') if config: cmd.append('-DCMAKE_BUILD_TYPE={0}'.format(config)) print('Running {0}'.format(' '.join(cmd))) subprocess.run(cmd, cwd=app_dir, check=True) # CMake build. num_cpus = str(os.cpu_count()) cmd = ['cmake', '--build', build_dir, '-j', num_cpus, '--config', config] print('Running {0}'.format(' '.join(cmd))) subprocess.run(cmd, cwd=app_dir, check=True) def main(): args = parse_cmdline_args() if not is_path_valid_for_cmake(args.sdk_dir): print('SDK path provided is not valid. ' 'Could not find a CMakeLists.txt at the root level.\n' 'Please check the argument to "--sdk_dir".') sys.exit(1) if not is_path_valid_for_cmake(args.app_dir): print('App path provided is not valid. ' 'Could not find a CMakeLists.txt at the root level.\n' 'Please check the argument to "--app_dir"') sys.exit(1) if is_sdk_path_source(args.sdk_dir): print('SDK path provided is a Firebase C++ source directory. Building...') build_source_vcpkg_dependencies(args.sdk_dir, args.arch, args.msvc_runtime_library) build_app_with_source(args.app_dir, args.sdk_dir, args.build_dir, args.arch, args.msvc_runtime_library, args.config, args.target_format) else: validate_prebuilt_args(args.arch, args.config) print('SDK path provided is Firebase C++ prebuilt libraries. Building...') build_app_with_prebuilt(args.app_dir, args.sdk_dir, args.build_dir, args.arch, args.msvc_runtime_library, args.config) print('Build successful!\n' 'Please find your executables in the build directory: {0}'.format( os.path.join(args.app_dir, args.build_dir))) def parse_cmdline_args(): """Parse command line arguments.""" parser = argparse.ArgumentParser(description='Install Prerequisites for ' 'building cpp sdk.') parser.add_argument('--sdk_dir', help='Path to Firebase SDK - source or ' 'prebuilt libraries.', type=os.path.abspath) parser.add_argument('--app_dir', help="Path to application to build " "(directory containing application's CMakeLists.txt", type=os.path.abspath) parser.add_argument('-a', '--arch', default='x64', help='Platform architecture (x64, x86)') parser.add_argument('--msvc_runtime_library', default='static', help='Runtime library for MSVC ' '(static(/MT) or dynamic(/MD)') parser.add_argument('--build_dir', default='build', help='Output build directory') parser.add_argument('--config', default='Release', help='Release/Debug config') parser.add_argument('--target_format', default=None, help='(Mac only) whether to output frameworks (default)' 'or libraries.') args = parser.parse_args() return args if __name__ == '__main__': main()