# # All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or # its licensors. # # For complete copyright and license terms please see the LICENSE at the root of this # distribution (the "License"). All use of this software is governed by the License, # or, if provided, by the license below or the license accompanying this file. Do not # remove or modify any license notices. This file is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # System Imports import configparser import glob import json import os import re import string import fnmatch from collections import OrderedDict # waflib imports from waflib import Configure, Context, Utils, Errors, Node from waflib.TaskGen import feature, after_method from waflib.Configure import ConfigurationContext, conf, Logs from waflib.Errors import ConfigurationError, WafError # lmbrwaflib imports from lmbrwaflib.cry_utils import get_configuration, append_unique_kw_entry, append_to_unique_list from lmbrwaflib.third_party_sync import P4SyncThirdPartySettings from lmbrwaflib.utils import parse_json_file, write_json_file, calculate_file_hash, calculate_string_hash from lmbrwaflib.gems import GemManager from lmbrwaflib.settings_manager import LUMBERYARD_SETTINGS # misc imports from waf_branch_spec import BINTEMP_FOLDER, BINTEMP_CACHE_3RD_PARTY ALIAS_SEARCH_PATTERN = re.compile(r'(\$\{([\w]*)})') # Map platforms to their short version names in 3rd party PLATFORM_TO_3RD_PARTY_SUBPATH = { # win_x64 host platform 'win_x64_clang' : 'win_x64/vc140', 'win_x64_vs2017' : 'win_x64/vc140', # Not an error, VS2017 links with VS2015 binaries 'win_x64_vs2019' : 'win_x64/vc140', 'android_armv8_clang': 'win_x64/android_ndk_r15/android-21/arm64-v8a/clang-3.8', # osx host platform 'darwin_x64' : 'osx/darwin-clang-703.0.31', 'ios' : 'osx/ios-clang-703.0.31', 'appletv' : 'osx/appletv-clang-703.0.31', # linux host platform 'linux_x64' : 'linux/clang-3.8'} CONFIGURATION_TO_3RD_PARTY_NAME = {'debug' : 'debug', 'profile': 'release', 'release': 'release'} CONFIG_FILE_SETUP_ASSISTANT_CONFIG = "SetupAssistantConfig.json" CONFIG_SETUP_ASSISTANT_USER_PREF = "SetupAssistantUserPreferences.ini" @conf def get_platform_to_3rd_party_subpath(ctx): ctx.add_restricted_3rd_party_subpaths(PLATFORM_TO_3RD_PARTY_SUBPATH) return PLATFORM_TO_3RD_PARTY_SUBPATH def trim_lib_name(platform, lib_name): """ Trim the libname if it meets certain platform criteria :param platform: The platform key to base the trim decision on :param libname: The libname to conditionally trim :return: """ if platform.startswith('win_x64'): return lib_name if platform.startswith('xenia'): return lib_name if not lib_name.startswith('lib'): return lib_name return lib_name[3:] def get_third_party_platform_name(ctx, waf_platform_key): """ Get the third party platform name for a waf platform name :param ctx: The current context :param waf_platform_key: The waf platform key :return: """ if waf_platform_key not in ctx.get_platform_to_3rd_party_subpath(): ctx.fatal('Platform {} does not have a mapped sub path for 3rd Party Static Libraries in {}/wscript'.format(waf_platform_key, ctx.path.abspath())) return ctx.get_platform_to_3rd_party_subpath()[waf_platform_key] def get_third_party_configuration_name(ctx, waf_configuration_key): """ Get the 3rd party configuration name for a waf configuration name :param ctx: The current context :param waf_configuration_key: The waf configuration key :return: """ configuration_settings = LUMBERYARD_SETTINGS.get_build_configuration_setting(waf_configuration_key) return configuration_settings.third_party_config def evaluate_node_alias_map(lib_root, lib_name): """ Given a root of a lib config, perform an evaluation of all the aliases and produce a map of its value :param lib_root: The lib root :return: The map of aliased values """ def _process_alias_value(name_alias_map, name_alias_value, visit_stack): alias_match = ALIAS_SEARCH_PATTERN.search(name_alias_value) if alias_match is not None: # This contains an aliased value alias_key = alias_match.group(2) # If the value is another alias if alias_key not in name_alias_map: # If the alias is not in the map, this is an error raise RuntimeError('Invalid alias value {} for 3rd party library {}'.format(name_alias_value, lib_name)) if alias_key in visit_stack: # If the alias is in the stack, this is a recursive error raise RuntimeError('Recursive alias value error {} for 3rd party library {}'.format(name_alias_value, lib_name)) visit_stack.append(alias_key) aliased_values = [] aliased_names = name_alias_map[alias_key] if isinstance(name_alias_map[alias_key], list) else [name_alias_map[alias_key]] for alias_value in aliased_names: pre_expansion_aliases = _process_alias_value(name_alias_map, alias_value, visit_stack) for pre_expansion_alias in pre_expansion_aliases: aliased_values.append(ALIAS_SEARCH_PATTERN.sub(pre_expansion_alias, name_alias_value)) visit_stack.pop() return aliased_values else: return [name_alias_value] # Scan the optional 'aliases' node off of the parent to look for aliases to build the map evaluated_name_alias_map = {} if 'aliases' not in list(lib_root.keys()): return evaluated_name_alias_map alias_node = lib_root['aliases'] # The first pass is to collect the pre-evaluated alias names for node_key in alias_node: node_alias_value = alias_node[node_key] if isinstance(node_alias_value, str): node_alias_value = [node_alias_value] evaluated_name_alias_map[node_key] = node_alias_value # Second pass, go through each list (possible recursively) and expand any aliased values in the list stack = [] for aliased_node_key in evaluated_name_alias_map: aliased_node_values = evaluated_name_alias_map[aliased_node_key] stack.append(aliased_node_key) values = [] for aliased_node_value in aliased_node_values: values += _process_alias_value(evaluated_name_alias_map, aliased_node_value, stack) evaluated_name_alias_map[aliased_node_key] = values stack.pop() return evaluated_name_alias_map CACHE_JSON_MODEL_AND_ALIAS_MAP = {} # Keep a cache of the 3rd party uselibs mappings to a tuple of the config file and the alias root map that applies to it CONFIGURED_3RD_PARTY_USELIBS = {} @conf def get_platform_folder_to_keyword_map(ctx): """ Get the cached map of platforms to their PAL platform folder and keyword, so we can attempt to search for platform-specific 3rd party fragments to combine with the input config file :param ctx: Context :return: Map of platform names to their keyword """ try: return ctx.platform_folder_to_keyword_map except AttributeError: working_map = {} target_platforms = ctx.get_all_target_platforms(False) for target_platform in target_platforms: assert 'platform_folder' in target_platform.attributes assert 'platform_keyword' in target_platform.attributes platform_folder = target_platform.attributes['platform_folder'] platform_keyword = target_platform.attributes['platform_keyword'] working_map[platform_folder] = platform_keyword ctx.platform_folder_to_keyword_map = working_map return ctx.platform_folder_to_keyword_map @conf def get_3rd_party_config_record(ctx, lib_config_file): """ Read a config and parse its alias map. Also provide a caching mechanism to retrieve the config dictionary, list of uselib names, and alias map. :param ctx: Config context :param lib_config_file: The full pall of the config file to read :return: The parsed json dictionary, list of uselib names, and the map of alias values if any """ if lib_config_file in CACHE_JSON_MODEL_AND_ALIAS_MAP: return CACHE_JSON_MODEL_AND_ALIAS_MAP[lib_config_file] lib_info = ctx.parse_json_file(lib_config_file) assert "name" in lib_info platform_folder_to_keyword_map = ctx.get_platform_folder_to_keyword_map() config_file_name = lib_config_file.name file_base_name, file_ext = os.path.splitext(config_file_name) platform_base_node = lib_config_file.parent.make_node('Platform') # Only the following sections at the root level of the config file is 'mergeable' mergeable_keys = ['platform', 'aliases', 'configuration_settings'] for platform_folder, platform_keyword in list(platform_folder_to_keyword_map.items()): # For each platform, check if there exists a platform fragment config to attempt to merge check_fragment_filename = '{}_{}{}'.format(file_base_name, platform_keyword, file_ext) check_fragment_path = os.path.join(platform_base_node.abspath(), platform_folder, check_fragment_filename) if not os.path.exists(check_fragment_path): continue # Parse the platform fragment and attempt to merge the mergeable sections together lib_platform_fragment_node = platform_base_node.make_node('{}/{}'.format(platform_folder, check_fragment_filename)) config_plaform_fragment_json = ctx.parse_json_file(lib_platform_fragment_node) for level_one_key, level_one_dict in list(config_plaform_fragment_json.items()): # Validate/Match the root (level 1) key for merging if not isinstance(level_one_dict, dict): raise Errors.WafError("3rd Party Platform Fragment file '{}' has an " "invalid key '{}'. Fragment files may only contain " "dictionaries of values.".format(lib_platform_fragment_node.abspath(), level_one_key)) if level_one_key not in mergeable_keys: raise Errors.WafError("3rd Party Platform Fragment file '{}' has an " "unmergeable key '{}'. Valid mergeable keys are : {}".format(lib_platform_fragment_node.abspath(), level_one_key, ','.join(mergeable_keys))) target_root_dict = lib_info.setdefault(level_one_key, {}) for level_two_key, level_two_values in list(level_one_dict.items()): # For each value in level 2 dictionary, merge new items from the fragment to the base one. This only accepts new keys # and will not attempt to merge down even further if level_two_key in target_root_dict: raise Errors.WafError("Conflicting values for '{}' in config file '{}' " "and fragment file '{}'".format(level_two_key, lib_config_file.abspath(), lib_platform_fragment_node.abspath())) else: target_root_dict[level_two_key] = level_two_values uselib_names = [userlib_name.upper() for userlib_name in (lib_info["name"] if isinstance(lib_info["name"], list) else [lib_info["name"]])] alias_map = evaluate_node_alias_map(lib_info, lib_config_file) CACHE_JSON_MODEL_AND_ALIAS_MAP[lib_config_file] = (lib_info, uselib_names, alias_map) return lib_info, uselib_names, alias_map @conf def mark_3rd_party_config_for_autoconf(ctx): # Make sure we track 3rd party jsons for auto configure if not hasattr(ctx, 'additional_files_to_track'): ctx.additional_files_to_track = [] platform_folder_to_keyword_map = ctx.get_platform_folder_to_keyword_map() config_3rdparty_folder = ctx.engine_node.make_node('_WAF_/3rdParty') platform_base_node = config_3rdparty_folder.make_node('Platform') config_file_nodes = config_3rdparty_folder.ant_glob('*.json') for config_file_node in config_file_nodes: # Track each base 3rd party config node ctx.additional_files_to_track.append(config_file_node) # Track any platform fragment files for the config config_file_name = config_file_node.name file_base_name, file_ext = os.path.splitext(config_file_name) for platform_folder, platform_keyword in list(platform_folder_to_keyword_map.items()): # For each platform, check if there exists a platform fragment config to attempt to merge check_fragment_filename = '{}_{}{}'.format(file_base_name, platform_keyword, file_ext) check_fragment_path = os.path.join(platform_base_node.abspath(), platform_folder, check_fragment_filename) if not os.path.exists(check_fragment_path): continue # Track the platform fragment file for auto configure as well lib_platform_fragment_node = platform_base_node.make_node('{}/{}'.format(platform_folder, check_fragment_filename)) ctx.additional_files_to_track.append(lib_platform_fragment_node) # Also track Setup Assistant Config File setup_assistant_config_node = ctx.engine_node.make_node('SetupAssistantConfig.json') ctx.additional_files_to_track.append(setup_assistant_config_node) @conf def read_and_mark_3rd_party_libs(ctx): def _process(config_folder_node, config_file_abs, path_alias_map): filename = os.path.basename(config_file_abs) # Attempt to load the 3rd party configuration lib_config_file = config_folder_node.make_node(filename) config, uselib_names, alias_map = get_3rd_party_config_record(ctx, lib_config_file) # Mark all the uselib names as processed for uselib_name in uselib_names: CONFIGURED_3RD_PARTY_USELIBS[uselib_name] = (lib_config_file, path_alias_map) config_3rdparty_folder = ctx.engine_node.make_node('_WAF_/3rdParty') config_3rdparty_folder_path = config_3rdparty_folder.abspath() config_files = glob.glob(os.path.join(config_3rdparty_folder_path, '*.json')) if ctx.is_engine_local(): root_alias_map = {'ROOT': ctx.srcnode.abspath()} else: root_alias_map = {'ROOT': ctx.engine_path} for config_file in config_files: _process(config_3rdparty_folder, config_file, root_alias_map) # Read the 3rd party configs with export 3rd party set to true all_gems = GemManager.GetInstance(ctx).gems for gem in all_gems: ctx.root.make_node(gem.abspath).make_node('3rdParty') gem_3p_abspath = os.path.join(gem.abspath, '3rdParty', '*.json') gem_3p_config_files = glob.glob(gem_3p_abspath) gem_3p_node = ctx.root.make_node(gem.abspath).make_node('3rdParty') gem_alias_map = {'ROOT': ctx.srcnode.abspath(), 'GEM': gem.abspath} for gem_3p_config_file in gem_3p_config_files: _process(gem_3p_node, gem_3p_config_file, gem_alias_map) return CONFIGURED_3RD_PARTY_USELIBS # Regex pattern to search for path alias tags (ie @ROOT@) in paths PATTERN_PATH_ALIAS_ = r'@(.*)@' REGEX_PATH_ALIAS = re.compile('@(.*)@') # The %LIBPATH macro only supports basic filenames with lowercase/uppercase letters, digits, ., and _. PATTERN_SEARCH_LIBPATH = r'\%LIBPATH\(([a-zA-Z0-9\.\_]*)\)' REGEX_SEARCH_LIBPATH = re.compile(PATTERN_SEARCH_LIBPATH) class ThirdPartyLibReader: """ Reader class that reads a 3rd party configuration dictionary for a 3rd party library read from a json file and applies the appropriate environment settings to consume that library """ def __init__(self, ctx, config_node, lib_root, lib_key, platform_key, configuration_key, alias_map, path_alias_map, warn_on_collision, binary_compatible_platforms, get_env_func, apply_env_func): """ Initializes the class :param ctx: Configuration context :param config_node: The node of trhe 3rd party config file being read in :param lib_root: The root level of the library configuration :param lib_key: The library key :param platform_key: The 'target' platform name (from waf_branch_spec) :param configuration_key: The build configuration key (from waf_branch_spec) :param alias_map: A map of aliases that was preprocessed from the root node before this method call :return: True,None: Library detected and applied False,String: Error occurred """ self.ctx = ctx self.lib_root = lib_root self.lib_key = lib_key self.platform_root = lib_root.get('platform', {}) self.platform_key = platform_key self.configuration_key = configuration_key self.name_alias_map = alias_map self.path_alias_map = path_alias_map self.warn_on_collision = warn_on_collision self.get_env_func = get_env_func self.apply_env_func = apply_env_func self.config_file = config_node.abspath() # Create an error reference message based on the above settings self.error_ref_message = "({}, platform {})".format(config_node.abspath(), self.platform_key) # Process the platform key (in case the key is an alias). If the platform value is a string, then it must start with a '@' to represent # an alias to another platform definition self.processed_platform_key = self.process_platform_key(binary_compatible_platforms, platform_key) def raise_config_error(self, message): raise RuntimeError("{}: {}".format(message, self.error_ref_message)) def get_platform_version_options(self): """ Gets the possible version options for the given platform target e.g. Android => the possible APIs the pre-builds were built against """ try: if self.ctx.is_android_platform(self.processed_platform_key): possible_android_apis = self.ctx.get_android_api_lib_list() possible_android_apis.sort() return possible_android_apis except: pass return None def detect_3rd_party_lib(self): """ Detect the 3rd party libs for the current platform and configuration. This method must be called per variant """ def _get_optional_boolean_flag(key_name, default): if key_name in self.lib_root: # Be robust and accept and actual boolean type or a string representation ('true', 't' ,or '1') if isinstance(self.lib_root[key_name], bool): required = self.lib_root[key_name] elif isinstance(self.lib_root[key_name], str): required = self.lib_root[key_name].lower() in ('true', 't', '1') else: required = default return required suppress_warning = _get_optional_boolean_flag("suppress_warning", False) non_release_only = _get_optional_boolean_flag("non_release_only", False) if self.processed_platform_key is None: return False, None try: # If the library is configured only for non-release (debug, profile, et all), but we are currently in release/performance, # then short circuit out and return without setting any environment if non_release_only and self.configuration_key.lower() in ["release", "release_dedicated", "performance", "performance_dedicated"]: return False, None # Validate that this is a valid target platform on this host platform if not self.ctx.is_target_platform_enabled(self.platform_key): raise Errors.WafError("Invalid platform {} on this host".format(self.platform_key)) # Locate the library key if "name" not in self.lib_root: self.raise_config_error("Missing required 'name' attribute in configuration file") lib_name = self.lib_root["name"] header_only_library = _get_optional_boolean_flag("header_only", False) lib_required = _get_optional_boolean_flag("lib_required", False) engine_configurations = _get_optional_boolean_flag("engine_configs", False) if engine_configurations: # This is the rare case where the library configuration matches the engine's configuration lib_configuration = self.configuration_key.split('_')[0] else: # This is the common case where the library only has debug and release, which matches the engine's debug and non-debug configurations is_debug = self.configuration_key.lower() in ["debug", "debug_dedicated", "debug_test", "debug_test_dedicated"] if is_debug: lib_configuration = "debug" else: lib_configuration = "release" self.apply_uselib_from_config(lib_configuration, lib_name, header_only_library, lib_required) except (RuntimeError, WafError) as err: if not suppress_warning: self.ctx.warn_once("Unable to process 3rd party config file '{}': {}".format(self.lib_key, str(err))) error_msg = None if suppress_warning else str(err) return False, error_msg if Logs.verbose > 1: Logs.pprint("GREEN", 'Detected 3rd Party Library {}'.format(self.lib_key)) return True, None def apply_uselib_from_config(self, lib_configuration, input_uselib_name, header_only_library, lib_required): """ Apply all the uselib related values (INCLUDES, LIB, etc) for a uselib based on the configuration :param lib_configuration: debug or release :param input_uselib_name: The base uselib name :param header_only_library: Flag to indicate this is a header only lib (no libs) :param lib_required: Flag to indicate that this is a required lib """ # Pre-process the required "source" key at the root if "source" not in self.lib_root: self.raise_config_error("Missing required 'source' attribute in configuration file") lib_rel_source = self.lib_root["source"] if len(lib_rel_source) == 0: self.raise_config_error("Invalid empty 'source' attribute in configuration file") # Pre-process the path lib_source_path = self.apply_optional_path_alias(lib_rel_source, None) # Since copy_extra commands are most likely duplicated across multiple uselibs in the same library definition file, we will cache # the values and apply aliases as much as possible to reduce overhead in the cache file copy_extra_cache = {} # Maintain a map of static libs based on the compound key ${platform}_${configuration} platform_config_lib_map = {} # Work on either a list of uselibs or a single uselib uselib_names = [input_uselib_name] if isinstance(input_uselib_name,str) else input_uselib_name def debug_log(message): Logs.debug('third_party: {}'.format(message)) debug_log('----------------') debug_log('Processing {} for {}_{}'.format(self.lib_key, self.platform_key, self.configuration_key)) for uselib_name in uselib_names: uselib_env_name = uselib_name.upper() # Find the most specific include path and apply it include_paths = self.get_most_specific_entry("includes", uselib_name, lib_configuration) self.apply_uselib_values_file_path(uselib_env_name, include_paths, "INCLUDES", lib_source_path) system_include_paths = self.get_most_specific_entry("system_includes", uselib_name, lib_configuration) self.apply_uselib_values_file_path(uselib_env_name, system_include_paths, "SYSTEM_INCLUDES", lib_source_path) # Read and apply the values for DEFINES_(uselib_name) defines = self.get_most_specific_entry("defines", uselib_name, lib_configuration) self.apply_uselib_values_general(uselib_env_name, defines, "DEFINES") if header_only_library: # If this is a header only library, then no need to look for libs/binaries return True, None # symlink_patterns symlink_patterns = self.get_most_specific_entry("symlink_patterns", uselib_name, lib_configuration) if symlink_patterns: self.apply_uselib_values_general(uselib_env_name, symlink_patterns, "SYMLINK_PATTERNS") # Determine what kind of libraries we are using static_lib_paths = self.get_most_specific_entry("libpath", uselib_name, lib_configuration) static_lib_filenames = self.get_most_specific_entry("lib", uselib_name, lib_configuration) has_static_libs = bool(static_lib_paths and static_lib_filenames) import_lib_paths = self.get_most_specific_entry("importlibpath", uselib_name, lib_configuration) import_lib_filenames = self.get_most_specific_entry("import", uselib_name, lib_configuration) has_import_libs = bool(import_lib_paths and import_lib_filenames) shared_lib_paths = self.get_most_specific_entry("sharedlibpath", uselib_name, lib_configuration) shared_lib_filenames = self.get_most_specific_entry("shared", uselib_name, lib_configuration) has_shared_libs = bool(shared_lib_paths and shared_lib_filenames) framework_paths = self.get_most_specific_entry("frameworkpath", uselib_name, lib_configuration) frameworks = self.get_most_specific_entry("framework", uselib_name, lib_configuration) has_frameworks = bool(framework_paths and frameworks) apply_lib_types = [] # since the uselib system will pull all variable variants specified for that lib, we need to make sure the # correct version of the library is specified for the current variant. if has_static_libs and (has_shared_libs or has_import_libs): debug_log(' - Detected both STATIC and SHARED libs for {}'.format(uselib_name)) if self.ctx.is_build_monolithic(self.platform_key, self.configuration_key): apply_lib_types.append('static') else: apply_lib_types.append('shared') elif has_static_libs: apply_lib_types.append('static') elif has_shared_libs or has_import_libs: apply_lib_types.append('shared') if framework_paths: apply_lib_types.append('framework') # Apply any system libs that may be specified static_system_lib_names = self.get_most_specific_entry("system_static_lib", uselib_name, lib_configuration) if static_system_lib_names: self.apply_uselib_values_general(uselib_env_name, static_system_lib_names, "STLIB") shared_system_lib_names = self.get_most_specific_entry("system_lib", uselib_name, lib_configuration) if shared_system_lib_names: self.apply_uselib_values_general(uselib_env_name, shared_system_lib_names, "LIB") system_framework_names = self.get_most_specific_entry("system_framework", uselib_name, lib_configuration) if system_framework_names: self.apply_framework(uselib_env_name, system_framework_names) # apply the static libraries, if specified if 'static' in apply_lib_types: debug_log(' - Adding STATIC libs for {}'.format(uselib_name)) defines = self.get_most_specific_entry("defines_static", uselib_name, lib_configuration) self.apply_uselib_values_general(uselib_env_name, defines, "DEFINES") self.apply_uselib_values_file_path(uselib_env_name, static_lib_paths, 'STLIBPATH', lib_source_path) self.apply_lib_based_uselib(static_lib_filenames, static_lib_paths, 'STLIB', uselib_env_name, lib_source_path, lib_required, False, lib_configuration, platform_config_lib_map) # apply the frameworks, if specified if 'framework' in apply_lib_types: debug_log(' - Adding FRAMEWORKS for {}'.format(uselib_name)) self.apply_uselib_values_file_path(uselib_env_name, framework_paths, "FRAMEWORKPATH", lib_source_path) self.apply_framework(uselib_env_name, frameworks) # Apply the link flags flag if any link_flags = self.get_most_specific_entry("linkflags", uselib_name, lib_configuration) self.apply_uselib_values_general(uselib_env_name, link_flags, "LINKFLAGS", lib_configuration, platform_config_lib_map) # Apply any optional c/cpp compile flags if any cc_flags = self.get_most_specific_entry("ccflags", uselib_name, lib_configuration) self.apply_uselib_values_general(uselib_env_name, cc_flags, "CFLAGS", lib_configuration, platform_config_lib_map) self.apply_uselib_values_general(uselib_env_name, cc_flags, "CXXFLAGS", lib_configuration, platform_config_lib_map) # apply the shared libraries, if specified if 'shared' in apply_lib_types: debug_log(' - Adding SHARED libs for {}'.format(uselib_name)) linkage_lib_paths = shared_lib_paths linkage_lib_filenames = shared_lib_filenames if import_lib_paths and import_lib_filenames: debug_log('- Adding IMPORT libs for {}'.format(uselib_name)) linkage_lib_paths = import_lib_paths linkage_lib_filenames = import_lib_filenames self.apply_uselib_values_file_path(uselib_env_name, linkage_lib_paths, "LIBPATH", lib_source_path) self.apply_lib_based_uselib(linkage_lib_filenames, linkage_lib_paths, "LIB", uselib_env_name, lib_source_path, lib_required, False, lib_configuration, platform_config_lib_map) defines = self.get_most_specific_entry("defines_shared", uselib_name, lib_configuration) self.apply_uselib_values_general(uselib_env_name, defines, "DEFINES") # Special case, specific to qt. Since we are copying the dlls as part of the qt5 bootstrap, we will ignore the shared variables if we are working on the qt5 lib if self.lib_key == 'qt5': continue # keep the SHAREDLIB* vars around to house the full library names for copying self.apply_uselib_values_file_path(uselib_env_name, shared_lib_paths, "SHAREDLIBPATH", lib_source_path) self.apply_uselib_values_general(uselib_env_name, shared_lib_filenames, "SHAREDLIB") # Read and apply the values for PDB_(uselib_name) pdb_filenames = self.get_most_specific_entry("pdb", uselib_name, lib_configuration) combined_paths = (static_lib_paths if static_lib_paths is not None else []) + (shared_lib_paths if shared_lib_paths is not None else []) + (import_lib_paths if import_lib_paths is not None else []) if len(combined_paths) > 0: self.apply_lib_based_uselib(pdb_filenames, combined_paths, "PDB", uselib_env_name, lib_source_path, False, True) copy_extras = self.get_most_specific_entry("copy_extra", uselib_name, lib_configuration) if copy_extras is not None: copy_extras_key = '.'.join(copy_extras) if copy_extras_key in copy_extra_cache: copy_extra_base = copy_extra_cache[copy_extras_key] uselib_env_key = 'COPY_EXTRA_{}'.format(uselib_env_name) self.apply_env_func(uselib_env_key, '@{}'.format(copy_extra_base)) else: copy_extra_cache[copy_extras_key] = uselib_env_name self.apply_copy_extra_values(lib_source_path, copy_extras, uselib_env_name) def get_most_specific_entry(self, key_base_name, uselib_name, lib_configuration, fail_if_missing=False): """ Attempt to get the most specific entry list based on platform and/or configuration for a library. :param key_base_name: Base name of the entry to lookup :param uselib_name: The name of the uselib this entry that is being looked up :param lib_configuration: Library configuration :param fail_if_missing: Raise a RuntimeError if the value is missing, otherwise its an optional existence check :return: The requested entry value list if not, empty string if not """ entry = None platform_node = None # Platform specific nodes are optional provided as long as we can eventually determine the value if "platform" in self.lib_root: # Has list of platforms in the config platform_root_node = self.lib_root["platform"] # Has the specific platform if self.processed_platform_key in platform_root_node: platform_node = platform_root_node[self.processed_platform_key] uselib_name_specific_entry_key = "{}/{}".format(uselib_name, key_base_name) configuration_specific_entry_key = "{}_{}".format(key_base_name, lib_configuration) uselib_name_and_configuration_specific_entry_key = "{}/{}_{}".format(uselib_name, key_base_name, lib_configuration) entry_keys = [ key_base_name, uselib_name_specific_entry_key, configuration_specific_entry_key, uselib_name_and_configuration_specific_entry_key, ] version_options = self.get_platform_version_options() if version_options: entry_keys_master = entry_keys[:] entry_keys = [] for key in entry_keys_master: entry_keys.append(key) for version in version_options: version_key = "{}/{}".format(key, version) entry_keys.append(version_key) # reverse the key order so the most specific is found first entry_keys.reverse() # Check against the value at the root level for key in entry_keys: if key in self.lib_root: entry = self.lib_root[key] break # Check against the value at the platform level if platform_node is not None: for key in entry_keys: if key in platform_node: entry = platform_node[key] break # Fix up the entry if its not in a list if entry is not None and not isinstance(entry, list): entry = [entry] # Validate if the entry was found or not if entry is None: if fail_if_missing: self.raise_config_error("Cannot determine {} for 3rd Party library {} and platform {}".format(key_base_name, self.lib_key, self.platform_key)) return entry else: # Apply any alias if the entry implies an alias vetted_list = [] for single_entry in entry: if isinstance(single_entry, str): alias_match = ALIAS_SEARCH_PATTERN.search(single_entry) if alias_match is not None: # This contains an aliased value alias_key = alias_match.group(2) if alias_key not in self.name_alias_map: raise RuntimeError("Invalid alias key {} for 3rd Party library {} and platform {}".format(alias_key, key_base_name, self.lib_key, self.platform_key)) aliased_names = self.name_alias_map[alias_key] if isinstance(self.name_alias_map[alias_key],list) else [self.name_alias_map[alias_key]] for aliased_name in aliased_names: updated_name = ALIAS_SEARCH_PATTERN.sub(aliased_name, single_entry) vetted_list.append(updated_name) else: # This is a normal value vetted_list.append(single_entry) else: vetted_list.append(single_entry) return vetted_list def apply_general_uselib_values(self, uselib_env_name, values, uselib_var, is_file_path, path_prefix, configuration, platform_config_lib_map): """ Given a list of values for a library uselib variable, apply it optionally as a file (with validation) or a value :param uselib_env_name: The uselib ENV environment key header (i.e. FOO for FOO_DEFINES, FOO_LIBATH) :param values: List of values to apply :param uselib_var: The uselib name (DEFINES, LIBPATH, etc) :param is_file_path: Is this a filepath? (vs value). Adds extra path validation if it is :param path_prefix: If this is a file, then the path prefix is used to represent the full path prefix :param configuration: The configuration for value being applied :param platform_config_lib_map: Optional platform configuration alias map """ if values is None: return for value in values: if is_file_path and path_prefix is not None: if value == '.': # Single dot means current dir relative to the path prefix apply_value = path_prefix else: apply_value = self.apply_optional_path_alias(value, path_prefix) elif configuration is not None and platform_config_lib_map is not None and '%LIBPATH' in value: apply_value = self.process_value_libpath(configuration, value, platform_config_lib_map) else: apply_value = value if is_file_path and not self.ctx.cached_does_path_exist(apply_value): self.raise_config_error("Invalid/missing {} value '{}'".format(uselib_var, apply_value)) uselib_env_key = '{}_{}'.format(uselib_var, uselib_env_name) if self.warn_on_collision and self.get_env_func(uselib_env_key): if Logs.verbose > 1: Logs.warn('[WARN] 3rd party uselib collision on key {}. ' 'The previous value ({}) will be overridden with ({})'.format(uselib_env_key, self.get_env_func(uselib_env_key), apply_value)) else: Logs.warn('[WARN] 3rd party uselib collision on key {}. The previous value will be overridden'.format(uselib_env_key)) self.apply_env_func(uselib_env_key, apply_value) def apply_uselib_values_file_path(self, uselib_env_name, values, uselib_var, path_prefix=None, configuration=None, platform_config_lib_map=None): """ Given a list of filepath related values for a library uselib variable, apply it optionally as a file (with validation) or a value :param uselib_env_name: The uselib ENV environment key header (i.e. FOO for FOO_DEFINES, FOO_LIBATH) :param values: List of values to apply :param uselib_var: The uselib name (LIBPATH, etc) :param path_prefix: If this is a file, then the path prefix is used to represent the full path prefix :param configuration: The configuration for value being applied to :param platform_config_lib_map: Optional platform configuration alias map """ self.apply_general_uselib_values(uselib_env_name, values, uselib_var, True, path_prefix, configuration, platform_config_lib_map) def apply_uselib_values_general(self, uselib_env_name, values, uselib_var, configuration=None, platform_config_lib_map=None): """ Given a list of values for a library uselib variable, apply it optionally as a value :param uselib_env_name: The uselib ENV environment key header (i.e. FOO for FOO_DEFINES, FOO_LIBATH) :param values: List of values to apply :param uselib_var: The uselib name (DEFINES, LIB, etc) :param configuration: The configuration for value being applied to :param platform_config_lib_map: Optional platform configuration alias map """ self.apply_general_uselib_values(uselib_env_name, values, uselib_var, False, None, configuration, platform_config_lib_map) def apply_lib_based_uselib(self, lib_filenames, lib_paths, uselib_var, uselib_env_name, lib_source_path, is_required, fullpath, configuration=None, platform_config_lib_map=None): """ Given a list of lib filenames, validate and apply the uselib value LIB_ against the library and platform :param lib_filenames: The list of file names from the library configuration :param lib_paths: The list of library paths from the library configuration :param uselib_var: The uselib variable to apply to :param uselib_env_name: The uselib environment to apply the uselib variable to :param lib_source_path: The root path to the library in case any of the lib paths do not use any aliases :param is_required: Flag to validate if the key is required or not :param fullpath: Is the path a full path (vs relative) :param configuration: Configuration to use to build the valid library full path dictionary (platform_config_lib_map) :param platform_config_lib_map: Map to collect the library full paths based on a compound key of ${}_${} :return: """ if lib_filenames is None: return for lib_filename in lib_filenames: uselib_key = "{}_{}".format(uselib_var, uselib_env_name) # Validate that the file exists lib_found_fullpath = None for lib_path in lib_paths: check_base_path = self.apply_optional_path_alias(lib_path, lib_source_path) lib_file_path = os.path.normpath(os.path.join(check_base_path, lib_filename)) if self.ctx.cached_does_path_exist(lib_file_path): lib_found_fullpath = lib_file_path break if lib_filename.endswith('.so'): # Special case: If we can't find an .so, then check for any versioned .so. There should be post-processing # during configure to establish a symlink-connection for check in os.listdir(check_base_path): if fnmatch.fnmatch(check, '{}.*'.format(lib_filename)): # We found a potential match, but make sure its not a '.a' file since clang will link to that if its present if os.path.splitext(check)[1] != '.a': lib_found_fullpath = lib_file_path self.ctx.warn_once("[WARN] shared library {} not found, but found '{}' instead. Assuming that this is the correct version.".format(lib_filename, check)) break if lib_found_fullpath: break if lib_found_fullpath is None: if is_required: self.raise_config_error("Invalid/missing library file '{}'".format(lib_filename)) else: continue if configuration is not None and platform_config_lib_map is not None: library_plat_conf_key = '{}_{}'.format(self.processed_platform_key, configuration) if library_plat_conf_key not in platform_config_lib_map: platform_config_lib_map[library_plat_conf_key] = [lib_found_fullpath] else: platform_config_lib_map[library_plat_conf_key].append(lib_found_fullpath) # the windows linker is different in the sense it only takes in .libs regardless of the library type if uselib_var == 'LIB': if self.platform_key.startswith('xenia'): import_lib_path = "{}.lib".format(os.path.splitext(lib_file_path)[0]) if not self.ctx.cached_does_path_exist(import_lib_path): continue if self.platform_key.startswith('win_x64'): import_lib_path = "{}.lib".format(os.path.splitext(lib_file_path)[0]) if not self.ctx.cached_does_path_exist(import_lib_path): continue if fullpath: self.apply_env_func(uselib_key, lib_found_fullpath) else: lib_parts = os.path.splitext(lib_filename) lib_name = trim_lib_name(self.processed_platform_key, lib_parts[0]) self.apply_env_func(uselib_key, lib_name) def apply_framework(self, uselib_env_name, frameworks): """ Apply a framework value to the env (darwin) :param uselib_env_name: The uselib env name :param frameworks: The framework list """ if frameworks is None: return framework_key = 'FRAMEWORK_{}'.format(uselib_env_name) for framework in frameworks: self.apply_env_func(framework_key, framework) def apply_copy_extra_values(self, src_prefix, values, uselib_env_name): """ Apply a custom copy_extra value to the env :param src_prefix: Prefix (path) to the source file(s) to copy :param values: The list of filenames to copy :param uselib_env_name: The uselib env name :return: """ uselib_env_key = 'COPY_EXTRA_{}'.format(uselib_env_name) for value in values: # String format for a copy_extra value is 'source[:destination]'. # Obtain source and destination normalized copy_extra_parts = value.split(':') if len(copy_extra_parts) == 2: source = os.path.normpath("{}/{}".format(src_prefix, copy_extra_parts[0])) destination = os.path.normpath(copy_extra_parts[1]) elif len(copy_extra_parts) == 1: source = os.path.normpath("{}/{}".format(src_prefix, copy_extra_parts[0])) destination = os.path.normpath("./") else: Logs.warn("[WARN] Invalid copy_extra '{}'".format(value)) return False value_norm = '{}:{}'.format(source, destination) self.apply_env_func(uselib_env_key, value_norm) def apply_optional_path_alias(self, path, path_prefix): """ Given a path, a default prefix, and an alias dictionary, apply the rules to a path based on whether the path has an alias that is defined in an alias dictionary, or apply the default logic of prepending the path with the path_prefix to get an absolute path :param path: The relative path as defined in the config file :param path_prefix: The path prefix to apply by default :return: Processed path depending on whether there exists an alias or not """ alias_match_groups = REGEX_PATH_ALIAS.search(path) if alias_match_groups is None: processed_path = os.path.normpath(os.path.join(path_prefix, path)) else: alias_key = alias_match_groups.group(1) if alias_key.startswith("3P:"): # The alias key refers to a 3rd party identifier third_party_identifier = alias_key.replace("3P:", "").strip() resolved_path, enabled, roles, optional = self.ctx.tp.get_third_party_path(self.platform_key, third_party_identifier) # If the path was not resolved, it could be an invalid alias (missing from the SetupAssistantConfig.json) if not resolved_path: raise Errors.WafError("[ERROR] Invalid 3rd party alias {}".format(alias_key)) # If the path was resolved, we still need to make sure the 3rd party is enabled based on the roles if not enabled and not optional: error_message = "3rd Party alias '{}' specified in {} is not enabled. Make sure that at least one of the " \ "following roles is enabled: [{}]".format(alias_key, self.config_file, ', '.join(roles)) raise Errors.WafError(error_message) processed_path = os.path.normpath(path.replace('@{}@'.format(alias_key), resolved_path)) elif alias_key not in self.path_alias_map: if path_prefix is None: processed_path = os.path.normpath(path) else: # if the alias key extracted does not match, revert to the basic processed_path processed_path = os.path.normpath(os.path.join(path_prefix, path)) else: replace_value = self.path_alias_map[alias_key] processed_path = os.path.normpath(path.replace('@{}@'.format(alias_key), replace_value)) return processed_path def process_value_libpath(self, configuration, value, platform_config_lib_map): """ Handle special processing for values for the value keywword %LIBPATH. %LIBPATH will be expanded to the defined lib path values detected in the current configuration :param configuration: The configuration being applied :param value: The value (must contain the %LIBATH expression) :param platform_config_lib_map: the library platform map to use to lookup the values :return: The applied value to %LIBPATH """ # If we found LIBPATH in the value, make sure it follows %LIBPATH() found_libpath = REGEX_SEARCH_LIBPATH.search(value) if found_libpath is None: self.raise_config_error("Invalid macro value for macro %LIBPATH value detected ()".format(value)) # Extract the library name libname_macro_parameter = found_libpath.group(1) # Lookup the collected full library paths that was calculated from collecting the library paths earlier libpath_key = '{}_{}'.format(self.processed_platform_key, configuration) if libpath_key not in platform_config_lib_map: self.raise_config_error("%LIBPATH macro was specified for a platform/configuration with no libraries defined") # There may be more than one library path, so search for the particular one platform_config_libs = platform_config_lib_map[libpath_key] evaluated_lib_fullpath = None for platform_config_lib in platform_config_libs: if platform_config_lib.endswith(libname_macro_parameter): evaluated_lib_fullpath = platform_config_lib if evaluated_lib_fullpath is None: self.raise_config_error("Invalid/missing library for $LIBPATH macro value {}".format(value)) # If there is any spaces in the full lib path, surround with double quotes if ' ' in evaluated_lib_fullpath: evaluated_lib_fullpath = '{}'.format(evaluated_lib_fullpath) # Perform the replacement of the macro to the actual value apply_value = re.sub(PATTERN_SEARCH_LIBPATH, evaluated_lib_fullpath, value) return apply_value def resolve_platform_alias(self, aliased_platform, visited): if not isinstance(aliased_platform, str): self.raise_config_error("Invalid platform alias {}. Must be in the form @ where " "represents an explicitly defined platform.".format(aliased_platform)) if not aliased_platform.startswith('@'): self.raise_config_error("Invalid platform alias format {}. Must be in the form @ where " "represents an explicitly defined platform.".format(aliased_platform)) platform_name = aliased_platform[1:] alias_target = self.platform_root.get(platform_name, None) if alias_target is None: self.raise_config_error("Invalid alias '{}'".format(aliased_platform)) if isinstance(alias_target, str): # Protect against cyclic dependencies if alias_target in visited: self.raise_config_error("Invalid cyclic platform alias detected {}.".format(aliased_platform)) # The target is potentially another alias visited.add(alias_target) return self.resolve_platform_alias(alias_target, visited) elif isinstance(alias_target, dict): # The target is a concrete target, return the de-alias key return aliased_platform[1:] else: self.raise_config_error("Invalid platform entry {}. Must be either a dictionary of settings or an alias.".format(alias_target)) def process_platform_key(self, binary_compatible_platforms, platform_key): # No platform root is declared, then the processed platform is the platform if not self.platform_root: return platform_key # There exists a 'platform' node in the 3rd party config. make sure that the input platform key exists in there platform_value = self.platform_root.get(platform_key, None) if platform_value is None: # The platform cannot be found, check if there are any 'binary_compatible_platforms' for this platform if binary_compatible_platforms: # Check if any of the binary compatible platforms match any that exists in the current platform root binary_compatible_platform_match = False for binary_compatible_platform in binary_compatible_platforms: if binary_compatible_platform in self.platform_root: binary_compatible_platform_match = True break if binary_compatible_platform_match: # If there is a compatible platform, attempt to resolve it visited = set() return self.resolve_platform_alias('@{}'.format(binary_compatible_platform), visited) else: # If there was no evaluated platform key, that means the configuration file was valid but did not have entries for this # particular platform. if Logs.verbose > 1: Logs.warn('[WARN] Platform {} not specified in 3rd party configuration file {}'.format( self.platform_key, self.config_file)) return None if isinstance(platform_value, str): visited = set() return self.resolve_platform_alias(platform_value, visited) elif isinstance(platform_value, dict): return platform_key else: self.raise_config_error("Invalid platform entry {}. Must be either a dictionary of settings or an alias.".format(platform_key)) ALIAS_SEARCH_PATTERN = re.compile(r'(\$\{([\w]*)})') def evaluate_node_alias_map(lib_root, lib_name): """ Given a root of a lib config, perform an evaluation of all the aliases and produce a map of its value :param lib_root: The lib root :return: The map of aliased values """ def _process_alias_value(name_alias_map, name_alias_value, visit_stack): alias_match = ALIAS_SEARCH_PATTERN.search(name_alias_value) if alias_match is not None: # This contains an aliased value alias_key = alias_match.group(2) # If the value is another alias if alias_key not in name_alias_map: # If the alias is not in the map, this is an error raise RuntimeError('Invalid alias value {} for 3rd party library {}'.format(name_alias_value, lib_name)) if alias_key in visit_stack: # If the alias is in the stack, this is a recursive error raise RuntimeError('Recursive alias value error {} for 3rd party library {}'.format(name_alias_value, lib_name)) visit_stack.append(alias_key) aliased_values = [] aliased_names = name_alias_map[alias_key] if isinstance(name_alias_map[alias_key], list) else [name_alias_map[alias_key]] for alias_value in aliased_names: pre_expansion_aliases = _process_alias_value(name_alias_map, alias_value, visit_stack) for pre_expansion_alias in pre_expansion_aliases: aliased_values.append(ALIAS_SEARCH_PATTERN.sub(pre_expansion_alias, name_alias_value)) visit_stack.pop() return aliased_values else: return [name_alias_value] # Scan the optional 'aliases' node off of the parent to look for aliases to build the map evaluated_name_alias_map = {} if 'aliases' not in list(lib_root.keys()): return evaluated_name_alias_map alias_node = lib_root['aliases'] # The first pass is to collect the pre-evaluated alias names for node_key in alias_node: node_alias_value = alias_node[node_key] if isinstance(node_alias_value, str): node_alias_value = [node_alias_value] evaluated_name_alias_map[node_key] = node_alias_value # Second pass, go through each list (possible recursively) and expand any aliased values in the list stack = [] for aliased_node_key in evaluated_name_alias_map: aliased_node_values = evaluated_name_alias_map[aliased_node_key] stack.append(aliased_node_key) values = [] for aliased_node_value in aliased_node_values: values += _process_alias_value(evaluated_name_alias_map, aliased_node_value, stack) evaluated_name_alias_map[aliased_node_key] = values stack.pop() return evaluated_name_alias_map # Maintain a list of all the uselib prefixes that we apply to the env per platform/config ALL_3RD_PARTY_USELIB_PREFIXES = ( 'INCLUDES', 'SYSTEM_INCLUDES' 'DEFINES', 'LIBPATH', 'FRAMEWORKPATH', 'LINKFLAGS', 'SHAREDLIBPATH', 'SHAREDLIB', 'PDB', 'COPY_EXTRA' ) REGISTERED_3RD_PARTY_USELIB = set() @conf def register_3rd_party_uselib(ctx, use_name, target_platform, *k, **kw): """ Register a 3rd party uselib manually :param ctx configuration context :param use_name The uselib name (ie library name) to register :param target_platform The target platform for special library name handling. :param k,kw: keyword inputs """ def _apply_to_list(uselib_prefix, value_key): """ Apply a general value to a uselib key :param uselib_prefix: uselib prefix (category, ie DEFINES_, INCLUDES_, etc) :param value_key: The value from the keyword input to extract the value """ if value_key not in kw: return value = kw[value_key] if isinstance(value, list): ctx.env['{}_{}'.format(uselib_prefix, use_name)] = value else: ctx.env['{}_{}'.format(uselib_prefix, use_name)] = [value] def _apply_lib_to_list(lib_uselib_prefix, lib_key, libpath_uselib_prefix, libpath_key, process_libname): """ Apply the uselib to a library and its library path. The library value must be in the form of its full filename :param lib_uselib_prefix: The uselib prefix for the library declaration :param lib_key: The kw keyname to get the library name(s) :param libpath_uselib_prefix: The uselib prefix for the library path(s) :param libpath_key: The kw keyname to get the library path(s) """ if lib_key not in kw or libpath_key not in kw: return # Convert the values to a list always lib_value = kw[lib_key] libs_to_process = lib_value if isinstance(lib_value, list) else [lib_value] libpath_value = kw[libpath_key] libpaths_to_process = libpath_value if isinstance(libpath_value, list) else [libpath_value] # Check each lib's path validity and adjust the lib name if necessary (the lib values are the actual filenames) processed_libs = [] for lib_to_process in libs_to_process: path_valid = False for libpath in libpaths_to_process: if ctx.cached_does_path_exist(os.path.join(libpath, lib_to_process)): path_valid = True break if not path_valid: ctx.warn_once('Invalid library path for uselib {}. This may cause linker errors.'.format(use_name)) continue processed_lib = os.path.splitext(trim_lib_name(target_platform, lib_to_process))[0] if process_libname else lib_to_process processed_libs.append(processed_lib) ctx.env['{}_{}'.format(lib_uselib_prefix, use_name)] = processed_libs ctx.env['{}_{}'.format(libpath_uselib_prefix, use_name)] = libpaths_to_process if 'lib' in kw and 'sharedlib' in kw: raise Errors.WafError("Cannot register both a static lib and a regular lib for the same library ({})".format(use_name)) if 'sharedlib' in kw and 'importlib' not in kw: raise Errors.WafError("Cannot register a shared library without declaring its import library({})".format(use_name)) if 'importlib' in kw and 'sharedlib' not in kw: raise Errors.WafError("Cannot register an import library without declaring its shared library({})".format(use_name)) # Apply the library specific values if 'lib' in kw: _apply_lib_to_list('STLIB', 'lib', 'STLIBPATH', 'libpath', True) elif 'sharedlib' in kw: _apply_lib_to_list('LIB', 'importlib', 'LIBPATH', 'sharedlibpath', True) _apply_lib_to_list('SHAREDLIB', 'sharedlib', 'SHAREDLIBPATH', 'sharedlibpath', False) # Apply the general uselib keys if provided _apply_to_list('INCLUDES', 'includes') _apply_to_list('SYSTEM_INCLUDES', 'system_includes') _apply_to_list('DEFINES', 'defines') _apply_to_list('FRAMEWORKPATH', 'frameworkpath') _apply_to_list('LINKFLAGS', 'linkflags') _apply_to_list('PDB', 'pdb') _apply_to_list('COPY_EXTRA', 'copy_extra') global REGISTERED_3RD_PARTY_USELIB REGISTERED_3RD_PARTY_USELIB.add(use_name) @conf def is_third_party_uselib_configured(ctx, use_name): """ Determine if a name is a configured uselib that was detected during the 3rd party framework initialization :param ctx: Context that contains both the env and the all_envs attributes :param use_name: The name to validate :return: True if its a valid uselib, False if not """ # First check against the evaluated 3rd party set if use_name in CONFIGURED_3RD_PARTY_USELIBS: return True if use_name in REGISTERED_3RD_PARTY_USELIB: return True # Check against the global env global_env_key = '' # Within all_envs, the key for the global env is a blank string third_party_uselibs = getattr(ctx.all_envs[global_env_key], 'THIRD_PARTY_USELIBS', []) if use_name in third_party_uselibs: return True # Second check the current env against all the possible combinations for prefix in ALL_3RD_PARTY_USELIB_PREFIXES: prefix_key = '{}_{}'.format(prefix, use_name) if prefix_key in ctx.env: return True return False # Attempt to retrieve configuration_settings for the uselib from either our cached tables in memory during # configuration or our context environment. @conf def get_configuration_settings(ctx, use_name): configdict = {} # During the configuration step the uselib .json file is parsed and cached in memory lib_config_file, _ = CONFIGURED_3RD_PARTY_USELIBS.get(use_name, (None, None)) if lib_config_file: config, uselib_names, alias_map = get_3rd_party_config_record(ctx, lib_config_file) configdict = config.get('configuration_settings', {}) # During the build step check for configuration_settings which were saved to the context environment else: third_party_uselib_additional_settings = getattr(ctx.all_envs[''], 'THIRD_PARTY_USELIB_SETTINGS', {}) configdict = third_party_uselib_additional_settings.get(use_name, {}) return configdict # A uselib can declare a 'configuration_settings' section which will be added to the kw dictionary # for modules consuming the uselib @conf def append_dependency_configuration_settings(ctx, use_name, kw): configdict = ctx.get_configuration_settings(use_name) for _key, _val in configdict.items(): append_unique_kw_entry(kw, _key, _val) # Weight table for our custom ordering of the json config file THIRD_PARTY_CONFIG_KEY_WEIGHT_TABLE = { 'name': 0, 'source': 1, 'description': 2, 'aliases': 3, 'includes': 10, 'includes_debug': 11, 'includes_profile': 12, 'includes_performance': 13, 'includes_release': 14, 'defines': 20, 'defines_debug': 21, 'defines_profile': 22, 'defines_performance': 23, 'defines_release': 24, 'lib_required': 30, 'shared_required': 31, 'platform': 32, 'libpath': 50, 'libpath_debug': 51, 'libpath_release': 52, 'lib': 53, 'lib_debug': 54, 'lib_release': 55, # Will start the key weight at 100 for the platforms just in case 'win_x64': 100, 'win_x64_vs2017': 101, 'win_x64_vs2019': 102, 'win_x64_clang': 103, 'xenia': 110, 'xenia_vs2017': 111, 'xenia_vs2019': 112, 'provo': 120, 'darwin_x64': 130, 'ios': 131, 'appletv': 132, 'android_armv8_clang': 140, 'linux_x64': 150, 'salem': 160 } def _get_config_key_weight(key): ''' Get the weight of a 3rd party config key so we can custom order it :param key: The key to evaluate :return: The key weight ''' if key in THIRD_PARTY_CONFIG_KEY_WEIGHT_TABLE: return THIRD_PARTY_CONFIG_KEY_WEIGHT_TABLE[key] else: return 99999 def generate_3p_config(tgen): ctx = tgen.bld def _compare_list(left, right): if not isinstance(left, list): return False if not isinstance(right, list): return False left_str = string.join(left, ':') right_str = string.join(right, ':') return left_str == right_str platform = ctx.env['PLATFORM'] # Short circuit out if its not a true build platform if platform == 'project_generator': return name = getattr(tgen, 'target', None) uselib_name = getattr(tgen, 'output_file_name', name) description = getattr(tgen, 'description', uselib_name) base_path = getattr(tgen, 'base_path', None) config_includes = getattr(tgen, 'config_includes', ['.']) config_defines = getattr(tgen, 'config_defines', []) if name is None or uselib_name is None or base_path is None: return config_filename = '{}.json'.format(uselib_name) config_3rdparty_folder = ctx.root.make_node(Context.launch_dir).make_node('_WAF_/3rdParty') config_file_node = config_3rdparty_folder.make_node(config_filename) config_file_abspath = config_file_node.abspath() # Determine the platform, config_platform, and config_configuration platform = ctx.env['PLATFORM'] if platform not in ctx.get_platform_to_3rd_party_subpath(): return config_platform = ctx.get_platform_to_3rd_party_subpath()[platform] configuration = get_configuration(ctx, name) config_configuration = LUMBERYARD_SETTINGS.get_build_configuration_setting(configuration).third_party_config output_filename = ctx.env['cxxstlib_PATTERN'] % uselib_name lib_path = 'build/{}/{}'.format(config_platform, config_configuration) if ctx.cached_does_path_exist(config_file_abspath): # The file exist, lookup the platform # open the config file contents target_config_file_node = config_3rdparty_folder.make_node('{}.json'.format(uselib_name)) config = ctx.parse_json_file(target_config_file_node) # Track if we need to update the file need_update = False if 'description' not in config: config['description'] = description need_update = True elif config['description'] != description: config['description'] = description need_update = True if 'includes' not in config: config['includes'] = config_includes need_update = True elif not _compare_list(config['includes'], config_includes): config['includes'] = config_includes need_update = True if 'defines' not in config: config['defines'] = config_defines need_update = True elif not _compare_list(config['defines'], config_defines): config['defines'] = config_defines need_update = True if 'platform' not in config: config['platform'] = {} need_update = True platforms = config['platform'] if platform not in platforms: # Create a new platform entry for the current platform without a configuration specifier cur_platform = { "libpath": [lib_path], "lib": [output_filename] } platforms[platform] = cur_platform need_update = True else: cur_platform = platforms[platform] if isinstance(cur_platform, str): # If the platform value is a string, then it must be a valid alias (@...) if not cur_platform.startswith('@'): raise Errors.WafError('Invalid platform name: {}'.format(cur_platform)) platform_key = cur_platform[1:] if platform_key not in platforms: raise Errors.WafError('Invalid platform name: {}'.format(cur_platform)) cur_platform = platforms[platform_key] # Platform exists. Depending on whats in the platform, do different actions config_specific_update_libpath_key = 'libpath_{}'.format(config_configuration) if 'libpath_debug' in cur_platform and 'libpath_release' in cur_platform: # Both debug and release configurations exist, we will update one of them if needed if cur_platform[config_specific_update_libpath_key][0] != lib_path: cur_platform[config_specific_update_libpath_key] = [lib_path] need_update = True elif 'libpath' in cur_platform: # Only one libpath exists, it may be either configuration. Check if we need to update the existing or # split into 2 if cur_platform['libpath'][0] != lib_path: # The libpath doesnt match, 'libpath' needs to be split replace_libpath_key = 'libpath_release' if config_configuration == 'debug' else 'libpath_debug' new_libpath_key = 'libpath_debug' if config_configuration == 'debug' else 'libpath_release' cur_platform[replace_libpath_key] = cur_platform['libpath'] cur_platform.pop('libpath', None) cur_platform[new_libpath_key] = [lib_path] need_update = True elif 'libpath_debug' in cur_platform or 'libpath_release' in cur_platform: # There is a config specific key already, but only one. if config_specific_update_libpath_key in cur_platform: # This is possibly updating the existing key if cur_platform[config_specific_update_libpath_key][0] != lib_path: cur_platform[config_specific_update_libpath_key] = [lib_path] need_update = True else: # This will create a new key cur_platform[config_specific_update_libpath_key] = [lib_path] need_update = True else: # If all else fails, that means there are no recognized libpath values. Create a default single one cur_platform[config_specific_update_libpath_key] = [lib_path] # Make sure the 'lib' value exists if 'lib' not in cur_platform: cur_platform['lib'] = [output_filename] need_update = True # Check if we need to update the lib value if cur_platform['lib'][0] != output_filename: cur_platform['lib'] = [output_filename] need_update = True else: # try to find the third party name from the dictionary based on the path third_party_name = '' try: third_party_root = ctx.tp.content.get("3rdPartyRoot") base_source = os.path.relpath(base_path, third_party_root) content_sdks = ctx.tp.content.get("SDKs") for key, value in content_sdks.items(): if os.path.normpath(value['base_source']) == base_source: third_party_name = key break except: pass # fallback to ROOT pathing if third_party_name: third_party_source = '@3P:{}@'.format(third_party_name) else: third_party_source = '@ROOT@/{}'.format(base_path) config = { 'name': uselib_name, 'source': third_party_source, 'description': description, 'includes': config_includes, 'defines': config_defines, 'lib_required': "True", 'shared_required': "False", 'platform': { platform : { 'libpath': [lib_path], 'lib': [output_filename] } } } need_update = True if need_update: # Level 1 : Order the root ordered_config = OrderedDict(sorted(list(config.items()), key=lambda t: _get_config_key_weight(t[0]))) # Level 2 : Order the platforms if 'platform' in ordered_config: config_platforms = ordered_config['platform'] ordered_config['platform'] = OrderedDict(sorted(list(config_platforms.items()), key=lambda t: _get_config_key_weight(t[0]))) for config_platform_name in ordered_config['platform']: config_platform_def = ordered_config['platform'][config_platform_name] if isinstance(config_platform_def, dict): ordered_config['platform'][config_platform_name] = OrderedDict(sorted(list(config_platform_def.items()), key=lambda t: _get_config_key_weight(t[0]))) config_content = json.dumps(ordered_config, indent=4, separators=(',', ': ')) try: config_file_node.write(config_content) except Exception as err: Logs.warn('[WARNING] Unable to update 3rd Party Configuration File {}:{}'.format(config_filename, err)) # The current version of the working 3rd party settings file in bin temp. CURRENT_3RD_PARTY_SETTINGS_VERSION = 1 # Mark any SDKs that will be exempt from any automatic updates EXEMPT_SDKS = ('FFmpeg') SETUP_ASSISTANT_PREF_SECTION = "General" SETUP_ASSISTANT_ENGINE_PATH_VALID = "SDKEnginePathValid" SETUP_ASSISTANT_ENGINE_PATH_3RD_PARTY = "SDKSearchPath3rdParty" @conf def get_setup_assistant_config_file_and_content(ctx): try: return ctx.setup_assistant_config_file_path, ctx.setup_assistant_config_content except AttributeError: # Validate the file path to the required SetupAssistantConfig file file_path = os.path.join(ctx.engine_node.abspath(), CONFIG_FILE_SETUP_ASSISTANT_CONFIG) if not os.path.exists(file_path): ctx.fatal('[ERROR] Cannot locate required config file {}'.format(CONFIG_FILE_SETUP_ASSISTANT_CONFIG)) # Parse & Validate the minimum schema content = parse_json_file(file_path) for required_key in ('SDKs', 'Capabilities'): if required_key not in content: ctx.fatal('[ERROR] Invalid setup assistant config file ({}). Missing "{}" key.'.format(file_path, required_key)) ctx.setup_assistant_config_file_path = file_path ctx.setup_assistant_config_content = content return ctx.setup_assistant_config_file_path, ctx.setup_assistant_config_content THIRD_PARTY_SETTINGS_KEY_VERSION = "Version" THIRD_PARTY_SETTINGS_3P_ROOT = "3rdPartyRoot" THIRD_PARTY_SETTINGS_3P_ROOT_HASH = "3rdPartyRootHash" THIRD_PARTY_SETTINGS_EC_HASH = "EnabledCapabilitiesHash" THIRD_PARTY_SETTINGS_AP_HASH = "AvailablePlatformsHash" THIRD_PARTY_SETTINGS_SA_SOURCE = "SetupAssistantSource" THIRD_PARTY_SETTINGS_SA_SOURCE_HASH = "SetupAssistantSourceHash" THIRD_PARTY_SETTINGS_WSCRIPT_HASH = "3rdPartyWafScriptHash" THIRD_PARTY_SETTINGS_CONFIGURED_PLATFORM_HASH = "ConfiguredPlatformsHash" THIRD_PARTY_SETTINGS_SDKS = "SDKs" class ThirdPartySettings: """ This class manages the working 3rd party file in BinTemp that tracks all of the absolute paths to the third party libraries, based on the settings in SetupAssistantConfig.json, and the root 3rd party path from SetupAssistantUserPreferences.ini if it exists. """ def __init__(self, ctx): self.ctx = ctx self.file_path_abs = os.path.join(Context.launch_dir, BINTEMP_FOLDER, "3rdParty.json") self.content = None self.third_party_root = None self.local_p4_sync_settings = None self.third_party_p4_config_file = os.path.join(self.ctx.engine_node.abspath(), "3rd_party_p4.ini") self.disable_p4_sync_settings = not os.path.exists(self.third_party_p4_config_file) def initialize(self): self.content = None available_platforms = [base_target_platform.name() for base_target_platform in self.ctx.get_all_target_platforms()] available_platforms_hash = calculate_string_hash(','.join(available_platforms)) if not os.path.exists(self.file_path_abs): # Create the initial settings file if it doesnt exist self.content = self.create_default(available_platforms, available_platforms_hash) else: read_content = parse_json_file(self.file_path_abs) # Check if either the 3rd party root changed or the setup assistant configuration's contents changed try: check_version = read_content.get(THIRD_PARTY_SETTINGS_KEY_VERSION) check_3rd_party_root = self.calculate_3rd_party_root() check_3rd_party_root_hash = read_content.get(THIRD_PARTY_SETTINGS_3P_ROOT_HASH) check_enabled_capabilities_hash = read_content.get(THIRD_PARTY_SETTINGS_EC_HASH) check_available_platforms_hash = read_content.get(THIRD_PARTY_SETTINGS_AP_HASH) check_setup_assistant_source = read_content.get(THIRD_PARTY_SETTINGS_SA_SOURCE) check_setup_assistant_source_hash = read_content.get(THIRD_PARTY_SETTINGS_SA_SOURCE_HASH) check_3rd_party_waf_script_hash = read_content.get(THIRD_PARTY_SETTINGS_WSCRIPT_HASH) check_configured_platforms_hash = read_content.get(THIRD_PARTY_SETTINGS_CONFIGURED_PLATFORM_HASH, '') if not check_3rd_party_root or not check_3rd_party_root_hash or \ not check_setup_assistant_source or not check_setup_assistant_source_hash or \ not check_enabled_capabilities_hash or not check_version: raise RuntimeWarning("Missing 3rd Party settings attributes") if check_version != CURRENT_3RD_PARTY_SETTINGS_VERSION: raise RuntimeWarning("Unsupported version") if check_available_platforms_hash != available_platforms_hash: raise RuntimeWarning("Available Platforms changed") # Create a hash of the third party lib so we can detect changes hash_digest_3rd_party_path = calculate_string_hash(check_3rd_party_root) if check_3rd_party_root_hash != hash_digest_3rd_party_path: raise RuntimeWarning("3rdPartyRoot value changed.") if not os.path.exists(check_setup_assistant_source): raise RuntimeWarning("SetupAssistantSource value invalid.") # Calculate the hash of the source setup assistant hash_src_setup_assistant_config = calculate_file_hash(check_setup_assistant_source) if hash_src_setup_assistant_config != check_setup_assistant_source_hash: raise RuntimeWarning("SetupAssistantSourceHash value changed.") enabled_capabilities = self.ctx.get_enabled_capabilities() enabled_capabilities_hash = calculate_string_hash(",".join(enabled_capabilities)) if enabled_capabilities_hash != check_enabled_capabilities_hash: raise RuntimeWarning("EnabledCapabilitiesHash value changed.") available_platform = sorted(self.ctx.get_all_target_platform_names()) available_platform_hash = calculate_string_hash(','.join(available_platform)) if available_platform_hash != check_configured_platforms_hash: raise RuntimeWarning("ConfiguredPlatformsHash value changed.") # Calculate the hash of this file waf_3rd_party_script_file = os.path.realpath(__file__) if not os.path.exists(waf_3rd_party_script_file): self.ctx.fatal('[ERROR] Unable to locate required file {}'.format(waf_3rd_party_script_file)) waf_3rd_party_script_hash = calculate_file_hash(waf_3rd_party_script_file) if waf_3rd_party_script_hash != check_3rd_party_waf_script_hash: raise RuntimeWarning("3rdPartyWafScriptHash value changed.") self.content = read_content except RuntimeWarning: Logs.info('[INFO] Regenerating 3rd Party settings file...') self.content = self.create_default(available_platforms, available_platforms_hash) def create_content(self, third_party_root, available_platforms, available_platforms_hash): def _do_roles_match(check_roles, match_roles): # Check if roles defined in SetupAssistantConfig.json match with the enabled roles if len(check_roles) > 0: matched = False for check_role in check_roles: if check_role in match_roles: matched = True break else: matched = True return matched # Validate the third party root if len(third_party_root) > 0 and not os.path.exists(third_party_root): self.ctx.fatal('[ERROR] 3rd Party root path is invalid') # Create a hash of the third party lib so we can detect changes hash_digest_3rd_party_path = calculate_string_hash(third_party_root) # Create a hash of this script file so we regenerate if any of this logic changes waf_3rd_party_script_file = os.path.realpath(__file__) if not os.path.exists(waf_3rd_party_script_file): self.ctx.fatal('[ERROR] Unable to locate required file {}'.format(waf_3rd_party_script_file)) waf_3rd_party_script_hash = calculate_file_hash(waf_3rd_party_script_file) # Read the setup assistant configuration and file hash to get the list of all of the 3rd party identifiers setup_assistant_config_file, setup_assistant_contents = self.ctx.get_setup_assistant_config_file_and_content() if not os.path.exists(setup_assistant_config_file): self.ctx.fatal('[ERROR] Unable to locate required file {}'.format(setup_assistant_config_file)) # Calculate the hash of the source setup assistant hash_src_setup_assistant_config = calculate_file_hash(setup_assistant_config_file) # Get the current enabled capabilities and build up the role list to filter on enabled_capabilities = self.ctx.get_enabled_capabilities() enabled_capabilities_hash = calculate_string_hash(",".join(enabled_capabilities)) setup_assistant_capabilities = setup_assistant_contents.get("Capabilities") if not setup_assistant_capabilities: self.ctx.fatal('[ERROR] Invalid setup assistant config file. Missing "Capabilities" key.') filter_roles = [] for setup_assistant_capability in setup_assistant_capabilities: capability_identifier = setup_assistant_capability.get("identifier") if not capability_identifier: continue # If enable capabitilies is not set, then check the default if this is enabled or not if len(enabled_capabilities) == 0 and not setup_assistant_capability.get("default", False): continue capability_categories = setup_assistant_capability.get("categories") if not capability_categories or "role" not in capability_categories: continue capability_tags = setup_assistant_capability.get("tags") if not capability_tags: continue for capability_tag in capability_tags: if capability_tag in enabled_capabilities and capability_tag not in filter_roles: filter_roles.append(capability_tag) # Filter out any platform-specific libraries that are not configured for the current host platform host_platform = Utils.unversioned_sys_platform() # Map the system platforms to the platform types in SetupAssistantConfig.json sys_platform_to_setup_os = {'darwin': 'macOS', 'win32': 'windows', 'linux': 'linux'} restricted_platform = sys_platform_to_setup_os[host_platform] available_platforms = sorted(self.ctx.get_all_target_platform_names()) available_platforms_hash = calculate_string_hash(','.join(available_platforms)) # Build up the SDKs section of the config third_party_sdks = {} sdk_index = 0 for sdk in setup_assistant_contents['SDKs']: sdk_index += 1 # Make sure the required identifier and source are present sdk_identifier = sdk.get("identifier", None) if not sdk_identifier: Logs.warn('[WARN] Missing "identifier" for "SDKs" entry {}'.format(sdk_index - 1)) continue sdk_source = sdk.get("source", None) if not sdk_source: Logs.warn('[WARN] Missing "source" for "SDKs" entry {}'.format(sdk_index - 1)) continue sdk_optional = sdk.get("optional", 0) == 1 sdk_roles = sdk.get("roles", []) role_matched = _do_roles_match(sdk_roles, filter_roles) # Check for any host platform restrictions at the root level host_os_list = sdk.get("hostOS", None) if host_os_list: # If there is an OS restriction, then make sure the current restricted platform qualifies os_qualifies = restricted_platform in host_os_list else: # If there is no OS restriction, then all platforms qualify to this point os_qualifies = True if not os_qualifies: continue # Check for any compiler restrictions sdk_compilers = sdk.get("compilers") if sdk_compilers: compiler_matched = False for available_platform in available_platforms: restricted_sdk_compiler = self.ctx.get_platform_attribute(available_platform, 'compiler', None) if restricted_sdk_compiler and restricted_sdk_compiler in sdk_compilers: compiler_matched = True break if not compiler_matched: continue targets = {} # the symlinks serve as check sources to make sure certain values exist. Create a list of # check-sources based on the symlinks sdk_symlinks = sdk.get("symlinks") if sdk_symlinks: for available_platform in available_platforms: # Apply the 3rd party rule for each target platform. check_files = [] current_platform_optional = sdk_optional current_platform_roles = [] sdk_symlink_roles = None # If there are symlinks defined, then treat them individually as targets for sdk_symlink in sdk_symlinks: # Check the compiler restriction if any sdk_symlink_compilers = sdk_symlink.get("compilers") if sdk_symlink_compilers: restricted_compiler = self.ctx.get_platform_attribute(available_platform, 'compiler', None) if restricted_compiler is not None and restricted_compiler not in sdk_symlink_compilers: # Compiler doesnt match any supported compiler continue # Check the hostOS restriction if any sdk_symlink_hostos = sdk_symlink.get("hostOS") if sdk_symlink_hostos: if restricted_platform not in sdk_symlink_hostos: # hostOS does not match the current host platform continue sdk_symlink_check_source = sdk_symlink.get("source") if not sdk_symlink_check_source: continue # Check if there is an optional override at the 'symlinks' level sdk_symlink_optional = sdk_symlink.get("optional", 1 if sdk_optional else 0) current_platform_optional = current_platform_optional and (sdk_symlink_optional == 1) sdk_symlink_roles = sdk_symlink.get("roles", None) if sdk_symlink_roles: current_platform_roles += [sdk_symlink_role for sdk_symlink_role in sdk_symlink_roles] sdk_symlink_example = sdk_symlink.get("exampleFile", "") if len(sdk_symlink_example) > 0: check_files.append(os.path.normpath(os.path.join(sdk_symlink_check_source, sdk_symlink_example))) if len(current_platform_roles) == 0: current_platform_roles = sdk_roles sdk_platform_role_matched = _do_roles_match(current_platform_roles, filter_roles) # Each target consists of the base sdk path, and the check sources target_entry = { "source": sdk_source, "check_files": check_files, "optional": current_platform_optional, "roles": current_platform_roles, "enabled": sdk_platform_role_matched } targets[available_platform] = target_entry else: # If there are no symlinks defined, then apply the same targets for all supporting platforms for available_platform in available_platforms: targets[available_platform] = {"source": sdk_source, "check_files": [], "optional": sdk_optional, "roles": sdk_roles, "enabled": role_matched } third_party_sdk = {"base_source": sdk_source, "targets": targets, "enabled": role_matched, "optional": sdk_optional, "roles": sdk_roles} third_party_sdks[sdk_identifier] = third_party_sdk third_party_settings_content = { THIRD_PARTY_SETTINGS_KEY_VERSION: CURRENT_3RD_PARTY_SETTINGS_VERSION, THIRD_PARTY_SETTINGS_3P_ROOT: third_party_root, THIRD_PARTY_SETTINGS_3P_ROOT_HASH: hash_digest_3rd_party_path, THIRD_PARTY_SETTINGS_EC_HASH: enabled_capabilities_hash, THIRD_PARTY_SETTINGS_AP_HASH: available_platforms_hash, THIRD_PARTY_SETTINGS_SA_SOURCE: os.path.join(self.ctx.engine_path, CONFIG_FILE_SETUP_ASSISTANT_CONFIG), THIRD_PARTY_SETTINGS_SA_SOURCE_HASH: hash_src_setup_assistant_config, THIRD_PARTY_SETTINGS_WSCRIPT_HASH: waf_3rd_party_script_hash, THIRD_PARTY_SETTINGS_CONFIGURED_PLATFORM_HASH: available_platforms_hash, THIRD_PARTY_SETTINGS_SDKS: third_party_sdks } return third_party_settings_content def calculate_3rd_party_root(self): """ Calculate where the 3rd party folder will be based on. The 3rd party root folder must have the 3rdParty.txt marker file :return: A third party root path """ def _search_3rd_party(starting_path): # Attempt to locate the 3rd party marker file from the current working directory # Move up to the root current_path = starting_path last_dir = None while current_path != last_dir: third_party_dir_name = os.path.join(current_path, '3rdParty') if os.path.exists(os.path.join(third_party_dir_name, '3rdParty.txt')): return third_party_dir_name last_dir = current_path current_path = os.path.dirname(current_path) return None def _get_default_third_party_root(): # If setup assistant user prefs does not exist, then attempt to search for a 3rd party root folder in # the current drive and use that as the default for now but warn the user third_party_root = _search_3rd_party(self.ctx.path.abspath()) if not third_party_root: raise Errors.ConfigurationError('[ERROR] Setup Assistant has not configured this project yet. You must run SetupAssistant ' 'first or specify a root 3rd Party folder (--3rdpartypath). ') self.ctx.warn_once('Setup Assistant has not configured this project yet. Defaulting to "{}" to continue this ' 'configuration. It is recommended that you run SetupAssistant to configure this project properly ' 'before continuing.'.format(third_party_root)) return third_party_root def _read_setup_assistant_user_pref(): setup_assistant_user_pref_filepath = os.path.join(self.ctx.get_engine_node().abspath(), CONFIG_SETUP_ASSISTANT_USER_PREF) if not os.path.exists(setup_assistant_user_pref_filepath): return _get_default_third_party_root() try: config_parser = configparser.ConfigParser() config_parser.read(setup_assistant_user_pref_filepath) engine_path_valid = config_parser.get(SETUP_ASSISTANT_PREF_SECTION, SETUP_ASSISTANT_ENGINE_PATH_VALID) if engine_path_valid != 'true': raise Errors.ConfigurationError('[ERROR] Setup Assistant reports that the engine path is invalid. Correct this in Setup Assistant before continuing') third_party_path = config_parser.get(SETUP_ASSISTANT_PREF_SECTION, SETUP_ASSISTANT_ENGINE_PATH_3RD_PARTY) if not os.path.exists(os.path.join(third_party_path, "3rdParty.txt")): raise Errors.ConfigurationError('[ERROR] The 3rd Party Root path ({}) configured in setup assistant is invalid. Correct this in Setup Assistant before continuing'.format(third_party_path)) return os.path.normpath(third_party_path) except configparser.Error as err: return _get_default_third_party_root() third_party_override = getattr(self.ctx.options, "bootstrap_third_party_override", None) if third_party_override: if not os.path.exists(os.path.join(third_party_override, "3rdParty.txt")): Logs.warn('[WARN] The 3rd party override (--3rdpartypath) path ({})is not a valid 3rd party root folder.'.format(third_party_override)) else: return os.path.normpath(third_party_override) return _read_setup_assistant_user_pref() def create_default(self, available_platforms, available_platforms_hash, third_party_root_override=None): """ Create the default 3rd party settings file :return: """ # Create an initial 3rd party root path if third_party_root_override: third_party_root_path = third_party_root_override else: third_party_root_path = self.calculate_3rd_party_root() third_party_settings_contents = self.create_content(third_party_root_path, available_platforms, available_platforms_hash) # Create the 3rd party settings write_json_file(third_party_settings_contents, self.file_path_abs) return third_party_settings_contents def validate_local(self, target_platform): """ Validate the settings against the actual file system :return: """ if not self.content: raise Errors.WafError('[ERROR] 3rd Party settings file not initialized properly') third_party_root = os.path.normpath(self.content.get("3rdPartyRoot")) content_sdks = self.content.get("SDKs") for sdk_identifier, sdk_content in content_sdks.items(): sdk_targets = sdk_content.get("targets") sdk_target = sdk_targets.get(target_platform) sdk_validated = True if sdk_target: sdk_target_source = sdk_target.get("source") sdk_check_files = sdk_target.get("check_files") sdk_enabled = sdk_target.get("enabled", False) sdk_optional = sdk_target.get("optional", True) for sdk_check_file in sdk_check_files: sdk_check_file_full_path = os.path.join(third_party_root, sdk_check_file) sdk_check_source_path = os.path.dirname(sdk_check_file_full_path) if not os.path.exists(sdk_check_file_full_path) and sdk_identifier not in EXEMPT_SDKS and sdk_enabled and not sdk_optional: self.ctx.warn_once("Unable to resolve 3rd Party path {} for library {}.".format(sdk_check_source_path, sdk_identifier)) sdk_validated = False if not sdk_validated: # One or more paths doesnt exist for this alias, do an encapsulating warning for the sdk alias self.ctx.warn_once("3rd Party alias '{}' invalid. It references one or more invalid local paths. Make sure the sdk is configured properly in Setup Assistant.".format(sdk_identifier)) def sync_from_p4(self, third_party_subpath): if self.local_p4_sync_settings is None: self.local_p4_sync_settings = P4SyncThirdPartySettings(self.content.get("3rdPartyRoot"), self.third_party_p4_config_file) if not self.local_p4_sync_settings.is_valid(): self.local_p4_sync_settings = None self.disable_p4_sync_settings = True return False self.local_p4_sync_settings.sync_3rd_party(third_party_subpath) return True def get_third_party_path(self, target_platform, identifier): """ Resolves a third party library configuration based on the target platform and 3rd party identifier (from SetupAssistantConfig.json) :param target_platform: The target platform to resolve :param identifier: The 3rd party alias identifier to lookup :return: Tuple representing a 3rd party setup: source path: The absolute path to the 3rd party library enabled: Flag to indicate if the library is enabled or not roles: Roles that are configured for the 3rd party library in SetupAssistantConfig.json optional: Flag to indicate if the sdk is optional (ie not a critical SDK) where no specs can be built """ if not self.content: raise Errors.WafError('3rd Party settings file not initialized properly') content_sdks = self.content.get("SDKs") sdk_content = content_sdks.get(identifier) if not sdk_content: raise Errors.WafError("3rd party identifier '{}' is not enabled for target platform '{}'".format(identifier, target_platform)) if not sdk_content.get("validated", True): raise Errors.WafError("3rd Party alias '{}' not validated locally.".format(identifier)) sdk_enabled = sdk_content.get('enabled') sdk_optional = sdk_content.get('optional') sdk_roles = sdk_content.get('roles') if not target_platform: # If the target platform is not specified, then use the base source sdk_source = sdk_content.get("base_source") if not sdk_source: raise Errors.WafError("[ERROR] 3rd Party settings file is invalid (Missing SDKs/{}/base_source)".format(identifier)) else: sdk_content_targets = sdk_content.get("targets") if not sdk_content_targets: raise Errors.WafError("[ERROR] 3rd Party settings file is invalid (Missing SDKs/{}/targets)".format(identifier)) sdk_content_target = sdk_content_targets.get(target_platform) if not sdk_content_target: raise Errors.WafError("[ERROR] 3rd Party settings file is invalid (Missing SDKs/targets/{})".format(target_platform)) sdk_source = sdk_content_target.get("source") # Get any target specific override for 'optional' and 'roles' sdk_optional = sdk_content_target.get('optional', sdk_optional) sdk_roles = sdk_content_target.get('roles', sdk_roles) third_party_root = self.content.get("3rdPartyRoot") sdk_source_full_path = os.path.normpath(os.path.join(third_party_root, sdk_source)) if not os.path.exists(sdk_source_full_path) and sdk_enabled and not sdk_optional: self.ctx.warn_once('3rd party alias {} references a missing local SDK path: {}. Make sure it is available through Setup Assistant'.format(identifier, sdk_source_full_path)) return sdk_source_full_path, sdk_enabled, sdk_roles, sdk_optional @conf def get_sdk_setup_assistant_roles_for_sdk(ctx, sdk_identifier_key): _, setup_assistant_contents = ctx.get_setup_assistant_config_file_and_content() sdks = setup_assistant_contents.get('SDKs') for sdk in sdks: sdk_identifier = sdk.get('identifier', None) if not sdk_identifier: raise AttributeError('Missing required "identifier" in sdk structure') if sdk_identifier != sdk_identifier_key: continue sdk_roles = sdk.get('roles') return sdk_roles raise AttributeError('Cannot find sdk by identifier {}'.format(sdk_identifier_key)) # Maintain a guid for the third party cache in case we change the schema for it so we can signal a rebuild THIRD_PARTY_CACHE_CONFIG_SCHEMA_UUID = "3DB6490A-260B-40AD-AC9F-1F8CD99ABDD7" THIRD_PARTY_CACHE_CONFIG_KEY_CONFIG_FINGERPRINT = 'config_fingerprint' THIRD_PARTY_CACHE_CONFIG_KEY_SAUP_FINGERPRINT = 'saup_fingerprint' THIRD_PARTY_CACHE_CONFIG_KEY_SAC_FINGERPRINT = 'sac_fingerprint' THIRD_PARTY_CACHE_CONFIG_KEY_PLATFORM_CONFIG_FINGERPRINT = 'pc_fingerprint' THIRD_PARTY_CACHE_CONFIG_KEY_ENGINE_CONFIG = 'engine_configurations' THIRD_PARTY_CACHE_CONFIG_KEY_CACHE_SCHEMA = 'cache_schema' THIRD_PARTY_CACHE_CONFIG_KEY_USELIB_NAMES = 'uselib_names' THIRD_PARTY_CACHE_CONFIG_KEY_NON_RELEASE_ONLY = 'non_release_only' THIRD_PARTY_CACHE_CONFIG_KEY_VARIANTS = 'variants' THIRD_PARTY_CACHE_CONFIG_KEY_BOOTSTRAP_PARAM = 'bootstrap_param' class ThirdPartyConfigEnvironmentCache: """ Class to support caching of Third Party library configurations that are processed during the configure phase. This class manages the results of the ThirdPartyConfigurationReader processing for each configuration/platform for each 3rd party file """ def __init__(self, config_file_path_abs, engine_root_abs, bootstrap_hash, lib_configurations_hash): # Capture the fingerprints for the files that affect the cache directly setup_assistant_config_file_path = os.path.join(engine_root_abs, CONFIG_FILE_SETUP_ASSISTANT_CONFIG) setup_assistant_user_preferences_file_path = os.path.join(engine_root_abs, CONFIG_SETUP_ASSISTANT_USER_PREF) if os.path.exists(setup_assistant_user_preferences_file_path): self.current_saup_fingerprint = calculate_file_hash(setup_assistant_user_preferences_file_path) else: self.current_saup_fingerprint = '' self.current_sac_fingerprint = calculate_file_hash(setup_assistant_config_file_path) self.current_config_fingerprint = calculate_file_hash(config_file_path_abs) self.current_bootstrap_param = bootstrap_hash self.platform_configurations_fingerprint = lib_configurations_hash # Working dictionary that will help optimize the storage size of the cache file self.variant_fingerprints_dict = {} # Calculate the cache file target config_filename = os.path.basename(config_file_path_abs) cache_path = os.path.join(engine_root_abs, BINTEMP_FOLDER, BINTEMP_CACHE_3RD_PARTY) if not os.path.isdir(cache_path): os.makedirs(cache_path) self.cached_config_file_path = os.path.join(cache_path, 'cache_3p.{}'.format(config_filename)) # On initialization, read the cached file if any, otherwise default to an empty cache dictionary if os.path.exists(self.cached_config_file_path): self.dictionary = parse_json_file(self.cached_config_file_path) self.cache_schema_id = self.dictionary.get(THIRD_PARTY_CACHE_CONFIG_KEY_CACHE_SCHEMA, THIRD_PARTY_CACHE_CONFIG_SCHEMA_UUID) if not self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_VARIANTS]: self.mark_cache_dirty() else: self.dictionary = { THIRD_PARTY_CACHE_CONFIG_KEY_CONFIG_FINGERPRINT: None, THIRD_PARTY_CACHE_CONFIG_KEY_SAC_FINGERPRINT: None, THIRD_PARTY_CACHE_CONFIG_KEY_SAUP_FINGERPRINT: None, THIRD_PARTY_CACHE_CONFIG_KEY_PLATFORM_CONFIG_FINGERPRINT: None, THIRD_PARTY_CACHE_CONFIG_KEY_CACHE_SCHEMA: THIRD_PARTY_CACHE_CONFIG_SCHEMA_UUID, THIRD_PARTY_CACHE_CONFIG_KEY_USELIB_NAMES: [], THIRD_PARTY_CACHE_CONFIG_KEY_NON_RELEASE_ONLY: False, THIRD_PARTY_CACHE_CONFIG_KEY_VARIANTS: {} } self.cache_schema_id = THIRD_PARTY_CACHE_CONFIG_SCHEMA_UUID # State attributes used during construction of the cache dictionary self.current_platform = None self.current_configuration = None def clear_variants(self): if THIRD_PARTY_CACHE_CONFIG_KEY_VARIANTS in self.dictionary: self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_VARIANTS].clear() else: self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_VARIANTS] = {} def clear_specific_variant(self, platform, configuration): if not platform or not configuration: Logs.warn('[WARN] No platform or configuration set for clear_specific_variants for the third party cache') return variants = self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_VARIANTS] variant = '{}_{}'.format(platform, configuration) if variant in variants: variants[variant] = {} def set_uselib_names(self, uselib_names): self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_USELIB_NAMES] = [uselib_name for uselib_name in uselib_names] def get_uselib_names(self): return self.dictionary.get(THIRD_PARTY_CACHE_CONFIG_KEY_USELIB_NAMES, []) def set_non_release_only(self, non_release_only=False): self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_NON_RELEASE_ONLY] = non_release_only def set_variant(self, platform, configuration): """ During the normal operation of ThirdPartyConfigReader, it applies the environment value to an active platform and configuration. This method sets the current platform/configuration state. :param platform: Current target platform :param configuration: Current target configuration """ self.current_platform = platform self.current_configuration = configuration def is_cache_dirty(self): """ Determine if the cache is dirty. This determines if the cache file needs to be updated and if its valid to apply values from the cache or not :return: True if its dirty, False if not """ if self.current_config_fingerprint != self.dictionary.get(THIRD_PARTY_CACHE_CONFIG_KEY_CONFIG_FINGERPRINT, None): return True if self.current_sac_fingerprint != self.dictionary.get(THIRD_PARTY_CACHE_CONFIG_KEY_SAC_FINGERPRINT, None): return True if self.current_saup_fingerprint != self.dictionary.get(THIRD_PARTY_CACHE_CONFIG_KEY_SAUP_FINGERPRINT, None): return True if self.current_bootstrap_param != self.dictionary.get(THIRD_PARTY_CACHE_CONFIG_KEY_BOOTSTRAP_PARAM, None): return True if self.platform_configurations_fingerprint != self.dictionary.get(THIRD_PARTY_CACHE_CONFIG_KEY_PLATFORM_CONFIG_FINGERPRINT, None): return True if THIRD_PARTY_CACHE_CONFIG_SCHEMA_UUID != self.dictionary.get(THIRD_PARTY_CACHE_CONFIG_KEY_CACHE_SCHEMA, THIRD_PARTY_CACHE_CONFIG_SCHEMA_UUID): return True return False def mark_cache_dirty(self): """ Mark the cache object as dirty by clearing current config """ self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_CONFIG_FINGERPRINT] = None def apply_env(self, platform, configuration, env): """ Apply the 3rd party environment to an environment if applicable. Otherwise, return False to signal the caller to process the 3rd party configuration and build up the cache. :param platform: current target platform :param configuration: current target (engine) configuration :param env: The env to apply the values to :return: True if the env is applied from the cache, False the platform/configuration variant is not set """ if self.is_cache_dirty(): return False variants = self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_VARIANTS] variant = '{}_{}'.format(platform, configuration.config_name()) if variant not in variants: return False if isinstance(variants[variant], str): # Check if this variant is a string that matches a concrete variant dictionary aliased_variant = variants[variant] # Make sure the alias points to an existing variant if aliased_variant not in variants: raise Errors.WafError("Invalid aliased variant '{}' for 3rd party cache file '{}' (Missing alias)".format(variant, self.cached_config_file_path)) # Make sure the variant that the alias points to is a concrete variant dictionary if isinstance(variants[aliased_variant], str): raise Errors.WafError("Invalid aliased variant '{}' for 3rd party cache file '{}' (Aliases cannot reference other aliases)".format(variant, self.cached_config_file_path)) variant_dict = variants.get(aliased_variant, None) elif not isinstance(variants[variant], dict): # If the variant is not a concrete definition (dictionary), then raise an error raise Errors.WafError("Invalid variant '{}' for 3rd party cache file '{}' (Bad definition, expecting an environment dictionary)".format(variant, self.cached_config_file_path)) else: # The variant represents a concrete dictionary variant_dict = variants[variant] # Apply the values from the concrete variant dictionary for key, value_list in list(variant_dict.items()): for value in value_list: env.append_unique(key, value) return True def apply_env_value(self, key, value): """ Apply a third party environment variable to the cache object :param key: The environment key :param value: The environment value """ if not self.current_platform or not self.current_configuration: Logs.warn('[WARN] No platform or configuration set for apply_value for the third party cache') return variants = self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_VARIANTS] variant = '{}_{}'.format(self.current_platform, self.current_configuration) if variant not in variants: variants[variant] = variant_dict = {} else: variant_dict = variants[variant] if key not in variant_dict: variant_dict[key] = [value] else: value_list = variant_dict[key] if value not in value_list: variant_dict[key].append(value) def get_current_variant_fingerprint(self): variants = self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_VARIANTS] variant = '{}_{}'.format(self.current_platform, self.current_configuration) if variant not in variants: return None variant_str = str(variants[variant]) variant_fingerprint = calculate_string_hash(variant_str) return variant_fingerprint def record_current_variant_fingerprint(self): """ Register the variant dictionary for the current platform and configurations. The fingerprints that will be registered will be used to optimize storage for the cache file later. This method must be called at the end of processing for each platform/configuration pair """ # Lookup the concrete variant for the current platform/configuration and capture a fingerprint variants = self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_VARIANTS] variant = '{}_{}'.format(self.current_platform, self.current_configuration) if variant not in variants: # No eligible variant return variant_str = str(variants[variant]) current_variant_fingerprint = calculate_string_hash(variant_str) # Apply the fingerprint to a dictionary. Each fingerprint entry will be a list of variants that match the fingerprint self.variant_fingerprints_dict.setdefault(current_variant_fingerprint, []).append(variant) def get_env_value(self, key): """ Get an environment value based on the current variant (see set_variant) :param key: The key to look up :return: The value for the key if it exists, None if it doesnt """ if not self.current_platform or not self.current_configuration: Logs.warn('[WARN] No platform or configuration set for apply_value for the third party cache') return None variants = self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_VARIANTS] variant = '{}_{}'.format(self.current_platform, self.current_configuration) if variant not in variants: variants[variant] = variant_dict = {} else: variant_dict = variants[variant] return variant_dict.get(key, None) def save(self): """ Save the cached third party environment if needed """ if not self.is_cache_dirty(): return # If there are no variants, this cache object is invalid. Do not save it if not self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_VARIANTS]: return # Go through the variant dictionary and update it so duplicate items can be aliased, reducing the size of the cache file else: optimized_variants = {} base_variant_for_variant_fingerprints = {} for variant_fingerprint, variant_list in list(self.variant_fingerprints_dict.items()): variant_list.sort() for variant in variant_list: if variant_fingerprint not in base_variant_for_variant_fingerprints: base_variant_for_variant_fingerprints[variant_fingerprint] = variant optimized_variants[variant] = self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_VARIANTS][variant] else: optimized_variants[variant] = base_variant_for_variant_fingerprints[variant_fingerprint] # Replace the dictionary of full concrete variants with the optimized one self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_VARIANTS] = optimized_variants self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_CONFIG_FINGERPRINT] = self.current_config_fingerprint self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_SAC_FINGERPRINT] = self.current_sac_fingerprint self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_SAUP_FINGERPRINT] = self.current_saup_fingerprint self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_BOOTSTRAP_PARAM] = self.current_bootstrap_param self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_PLATFORM_CONFIG_FINGERPRINT] = self.platform_configurations_fingerprint self.dictionary[THIRD_PARTY_CACHE_CONFIG_KEY_CACHE_SCHEMA] = THIRD_PARTY_CACHE_CONFIG_SCHEMA_UUID write_json_file(self.dictionary, self.cached_config_file_path) class CachedThirdPartyLibReader: """ This class manages the environment settings for a particular 3rd party configuration file and provides a caching of the environment evaluation from the source 3rd party configuration files. """ def __init__(self, ctx, config_file_path_node, path_prefix_map, warn_on_collision): self.config_file_path_node = config_file_path_node self.ctx = ctx self.path_prefix_map = path_prefix_map self.warn_on_collision = warn_on_collision self.lib_configurations = [] platforms_list = ctx.get_all_target_platform_names() platforms_list.sort() for platform in platforms_list: for configuration in ctx.get_supported_configurations(platform): append_to_unique_list(self.lib_configurations, configuration) self.lib_configurations.sort() lib_configurations_hash = calculate_string_hash(",".join(self.lib_configurations + platforms_list)) bootstrap_param = getattr(ctx.options, 'bootstrap_tool_param', '') bootstrap_3rd_party_override = getattr(ctx.options, 'bootstrap_third_party_override', '') bootstrap_hash = calculate_string_hash('{}/{}'.format(bootstrap_param, bootstrap_3rd_party_override)) self.cache_obj = ThirdPartyConfigEnvironmentCache(config_file_path_abs=config_file_path_node.abspath(), engine_root_abs=ctx.engine_path, bootstrap_hash=bootstrap_hash, lib_configurations_hash=lib_configurations_hash) if self.cache_obj.is_cache_dirty(): self.reset_cache_obj(ctx) def reset_cache_obj(self, ctx): """ Reset/Initialize the cache object that is managed by this reader from the source 3rd party configuration file. :param ctx: Configuration Context """ self.cache_obj.clear_variants() config, uselib_names, alias_map, = get_3rd_party_config_record(ctx, self.config_file_path_node) if not config: raise Errors.WafError('Unable to parse configuration file') self.cache_obj.set_uselib_names(uselib_names) non_release_only = config.get("non_release_only", False) self.cache_obj.set_non_release_only(non_release_only) # Iterate through the current supported platforms supported_platforms = [enabled_platform for enabled_platform in ctx.get_all_target_platforms()] # Before going through the list of current platforms, determine the actual platforms that the config is defined for # so we can skip processing those altogether config_platforms = list(config.get('platform', {}).keys()) or [supported_platform.platform for supported_platform in supported_platforms] for platform in supported_platforms: binary_compatible_platforms = platform.attributes.get('binary_compatible_platforms', []) # Skip any platform that is not supported by the current config if platform.platform not in config_platforms: binary_compatible_platform_found = False for binary_compatible_platform in binary_compatible_platforms: if binary_compatible_platform in config_platforms: binary_compatible_platform_found = True break if not binary_compatible_platform_found: continue for configuration in self.lib_configurations: # Skip any configuration that the platform does not support try: ctx.get_platform_configuration(platform.platform, configuration) except Errors.WafError: continue get_env_func = self.cache_obj.get_env_value apply_env_func = self.cache_obj.apply_env_value self.cache_obj.set_variant(platform.platform, configuration) result, err_msg = ThirdPartyLibReader(ctx=ctx, config_node=self.config_file_path_node, lib_root=config, lib_key=self.config_file_path_node.name, platform_key=platform.platform, configuration_key=configuration, alias_map=alias_map, path_alias_map=self.path_prefix_map, warn_on_collision=self.warn_on_collision, binary_compatible_platforms=binary_compatible_platforms, get_env_func=get_env_func, apply_env_func=apply_env_func).detect_3rd_party_lib() if err_msg and len(err_msg) > 0: # If there were any errrors, clear out any cached values set for this variant as they could be invalid now. self.cache_obj.clear_specific_variant(platform.platform, configuration) ctx.warn_once("Unable to configure uselibs '{}' for platform '{}'. This may cause build errors for " "modules that depend on those uselibs for that platform" .format(','.join(uselib_names), platform.platform)) # Record the variant for this platform/configuration for storage optimization later self.cache_obj.record_current_variant_fingerprint() self.cache_obj.save() def get_uselib_names(self): """ Get the uselib names extracted from the 3rd party configuration file :return: The uselib names from the 3rd party configuration """ return self.cache_obj.get_uselib_names() def apply(self, ctx, platform, configuration): """ Apply the environment values for the 3rd party environment definitions to the current configuration context :param ctx: The current configuration context :param platform: The current platform to apply for :param configuration: The current configuration to apply for """ if self.cache_obj.is_cache_dirty(): # If the cache is marked dirty, re-initialize it self.reset_cache_obj(ctx) if not self.cache_obj.apply_env(platform, configuration, ctx.env): # If we couldnt apply the environment to the platform/configuration, then that variant is not # available in the object cache. If the original config file is different from the one that # this cache was generated from, reset the cache and try again original_config_file_hash = calculate_file_hash(self.config_file_path_node.abspath()) cached_config_file_hash = self.cache_obj.current_config_fingerprint if original_config_file_hash != cached_config_file_hash: self.reset_cache_obj(ctx) self.cache_obj.apply_env(platform, configuration, ctx.env) @conf def get_uselib_third_party_reader_map(ctx): """ Get the map of uselibs to the CachedThirdPartyLibReader (see @CachedThirdPartyLibReader) :param ctx: The current configuration context :return: The map of all uselibs and their correspond """ try: return ctx.uselib_third_party_reader_map except AttributeError: ctx.uselib_third_party_reader_map = {} def _process_config_file(config_file_node, path_alias_map): # Attempt to load the 3rd party configuration warn_on_collision = False tp_reader = CachedThirdPartyLibReader(ctx, config_file_node, path_alias_map, warn_on_collision) uselib_names = tp_reader.get_uselib_names() for uselib_name in uselib_names: if uselib_name in ctx.uselib_third_party_reader_map: Logs.warn('Duplicate uselib library name "{}" detected from file {}'.format(uselib_name, config_file_node.abspath())) else: ctx.uselib_third_party_reader_map[uselib_name] = tp_reader if ctx.is_engine_local(): root_alias_map = {'ROOT': ctx.srcnode.abspath()} else: root_alias_map = {'ROOT': ctx.engine_path} # First look at the global 3rd party configurations config_3rdparty_folder = ctx.engine_node.make_node('_WAF_/3rdParty') config_3rdparty_folder_path = config_3rdparty_folder.abspath() global_config_files = glob.glob(os.path.join(config_3rdparty_folder_path, '*.json')) for global_config_file in global_config_files: config_file_name = os.path.basename(global_config_file) config_file_node = config_3rdparty_folder.make_node(config_file_name) _process_config_file(config_file_node, root_alias_map) # Read the 3rd party configs with export 3rd party set to true all_gems = GemManager.GetInstance(ctx).gems for gem in all_gems: ctx.root.make_node(gem.abspath).make_node('3rdParty') gem_3p_abspath = os.path.join(gem.abspath, '3rdParty', '*.json') gem_3p_config_files = glob.glob(gem_3p_abspath) gem_3p_node = ctx.root.make_node(gem.abspath).make_node('3rdParty') gem_alias_map = {'ROOT': root_alias_map['ROOT'], 'GEM': gem.abspath} for gem_3p_config_file in gem_3p_config_files: gem_3p_config_file_name = os.path.basename(gem_3p_config_file) gem_3p_config_file_node = gem_3p_node.make_node(gem_3p_config_file_name) _process_config_file(gem_3p_config_file_node, gem_alias_map) return ctx.uselib_third_party_reader_map COMPATIBILITY_USELIB_PREFIXES = [ 'INCLUDES', 'LIBPATH', 'LIB', 'STLIBPATH', 'STLIB', 'DEFINES', 'COPY_EXTRA', 'PDB', 'SHAREDLIBPATH', 'SHAREDLIB' ] @conf def check_platform_uselib_compatibility(ctx, uselib): """ Check if a uselib is supported on the current platform that is being built :param ctx: Context :param uselib: The uselib name to check :return: True if the uselib is supported by the current platform False if the uselib is not supported by the current platform, or the uselib does not exist None If this is a 'project_generator' platform or this call is invoked during configure """ platform_name = ctx.env['PLATFORM'] if platform_name == 'project_generator' or isinstance(ctx, Configure.ConfigurationContext): return None all_configuration_names = ctx.get_all_configuration_names() for configuration_name in all_configuration_names: platform_env_key = '{}_{}'.format(platform_name, configuration_name) if platform_env_key not in ctx.all_envs: continue for compat_uselib_prefix in COMPATIBILITY_USELIB_PREFIXES: check_uselib_key = '{}_{}'.format(compat_uselib_prefix, uselib) if check_uselib_key in ctx.all_envs[platform_env_key]: return True return False