From 95e1469eb8be6c7dd98d2bdb3bae4800e75cd2b7 Mon Sep 17 00:00:00 2001 From: Alexandru Croitor Date: Mon, 2 Mar 2020 18:06:46 +0100 Subject: CMake: Port most of the configure summary support Teaches configurejson2cmake about summaries / reports, so things like enabled features, configure sections, notes, etc. Add relevant CMake API for adding summary sections and entries, as well as configure reports. The commands record the passed data, and the data is later evaluated when the summary needs to be printed. This is needed, to ensure that all features are evaluated by the time the summary is printed. Some report and summary entries are not generated if they mention a feature that is explicitly exclduded by configurejson2cmake's feature mapping dictionary. This is to prevent CMake from failing at configure time when trying to evaluate an unknown feature. We should re-enable these in the future. A few custom report types are skipped by configurejson2cmake (like values of qmake CONFIG or buildParts). These will have to be addressed a case-by-case basis if still needed. Change-Id: I95d74ce34734d347681905f15a781f64b5bd5edc Reviewed-by: Leander Beernaert Reviewed-by: Joerg Bornemann --- cmake/QtBaseConfigureTests.cmake | 2 +- cmake/QtBuildInformation.cmake | 327 +++++++++++++++++++++ .../QtBuildInternals/QtBuildInternalsConfig.cmake | 2 - cmake/QtFeature.cmake | 7 + cmake/QtSetup.cmake | 1 + util/cmake/configurejson2cmake.py | 126 +++++++- 6 files changed, 460 insertions(+), 5 deletions(-) diff --git a/cmake/QtBaseConfigureTests.cmake b/cmake/QtBaseConfigureTests.cmake index 0d6bb1407b..0844c8df20 100644 --- a/cmake/QtBaseConfigureTests.cmake +++ b/cmake/QtBaseConfigureTests.cmake @@ -82,7 +82,7 @@ function(qt_run_config_test_architecture) set(QT_BASE_CONFIGURE_TESTS_VARS_TO_EXPORT ${QT_BASE_CONFIGURE_TESTS_VARS_TO_EXPORT} CACHE INTERNAL "Test variables that should be exported") list(JOIN _sub_architecture " " subarch_summary) - message(STATUS "Building for: ${QT_QMAKE_TARGET_MKSPEC} (${TEST_architecture_arch}, CPU features: ${subarch_summary})") + set_property(GLOBAL PROPERTY qt_configure_subarch_summary "${subarch_summary}") endfunction() diff --git a/cmake/QtBuildInformation.cmake b/cmake/QtBuildInformation.cmake index 097192b2ab..cf783168f0 100644 --- a/cmake/QtBuildInformation.cmake +++ b/cmake/QtBuildInformation.cmake @@ -1,11 +1,13 @@ function(qt_print_feature_summary) include(FeatureSummary) + # Show which packages were found. feature_summary(WHAT PACKAGES_FOUND REQUIRED_PACKAGES_NOT_FOUND RECOMMENDED_PACKAGES_NOT_FOUND OPTIONAL_PACKAGES_NOT_FOUND RUNTIME_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES) + qt_configure_print_summary() endfunction() function(qt_print_build_instructions) @@ -33,3 +35,328 @@ function(qt_print_build_instructions) message("\nIf reconfiguration fails for some reason, try to remove 'CMakeCache.txt' \ from the build directory \n") endfunction() + +function(qt_configure_print_summary) + # Evaluate all recorded commands. + qt_configure_eval_commands() + + # Show Qt-specific configure summary and any notes, wranings, etc. + if(__qt_configure_reports) + message("Configure summary:\n${__qt_configure_reports}") + endif() + if(__qt_configure_notes) + message("${__qt_configure_notes}") + endif() + if(__qt_configure_warnings) + message("${__qt_configure_warnings}") + endif() + if(__qt_configure_errors) + message("${__qt_configure_errors}") + endif() + message("") + if(__qt_configure_an_error_occurred) + message(FATAL_ERROR "Check the configuration messages for an error that has occurred.") + endif() +endfunction() + +# Takes a list of arguments, and saves them to be evaluated at the end of the configuration +# phase when the configuration summary is shown. +function(qt_configure_record_command) + # Don't record commands when only evaluating features of a configure.cmake file. + if(__QtFeature_only_evaluate_features) + return() + endif() + + get_property(command_count GLOBAL PROPERTY qt_configure_command_count) + + if(NOT DEFINED command_count) + set(command_count 0) + else() + math(EXPR command_count "${command_count}+1") + endif() + + set_property(GLOBAL PROPERTY qt_configure_command_${command_count} "${ARGV}") + set_property(GLOBAL PROPERTY qt_configure_command_count "${command_count}") +endfunction() + +function(qt_configure_eval_commands) + get_property(command_count GLOBAL PROPERTY qt_configure_command_count) + if(NOT command_count) + set(command_count 0) + endif() + set(command_index 0) + + while(command_index LESS command_count) + get_property(command_args GLOBAL PROPERTY qt_configure_command_${command_index}) + if(NOT command_args) + message(FATAL_ERROR + "Empty arguments encountered while processing qt configure reports.") + endif() + + list(POP_FRONT command_args command_name) + if(command_name STREQUAL ADD_SUMMARY_SECTION) + qt_configure_process_add_summary_section(${command_args}) + elseif(command_name STREQUAL END_SUMMARY_SECTION) + qt_configure_process_end_summary_section(${command_args}) + elseif(command_name STREQUAL ADD_REPORT_ENTRY) + qt_configure_process_add_report_entry(${command_args}) + elseif(command_name STREQUAL ADD_SUMMARY_ENTRY) + qt_configure_process_add_summary_entry(${command_args}) + elseif(command_name STREQUAL ADD_BUILD_TYPE_AND_CONFIG) + qt_configure_process_add_summary_build_type_and_config(${command_args}) + elseif(command_name STREQUAL ADD_BUILD_MODE) + qt_configure_process_add_summary_build_mode(${command_args}) + endif() + + math(EXPR command_index "${command_index}+1") + endwhile() + + # Propagate content to parent. + set(__qt_configure_reports "${__qt_configure_reports}" PARENT_SCOPE) + set(__qt_configure_notes "${__qt_configure_notes}" PARENT_SCOPE) + set(__qt_configure_warnings "${__qt_configure_warnings}" PARENT_SCOPE) + set(__qt_configure_errors "${__qt_configure_errors}" PARENT_SCOPE) + set(__qt_configure_an_error_occurred "${__qt_configure_an_error_occurred}" PARENT_SCOPE) +endfunction() + +macro(qt_configure_add_report message) + string(APPEND __qt_configure_reports "\n${message}") + set(__qt_configure_reports "${__qt_configure_reports}" PARENT_SCOPE) +endmacro() + +macro(qt_configure_add_report_padded label message) + qt_configure_get_padded_string("${__qt_configure_indent}${label}" "${message}" padded_message) + string(APPEND __qt_configure_reports "\n${padded_message}") + set(__qt_configure_reports "${__qt_configure_reports}" PARENT_SCOPE) +endmacro() + +function(qt_configure_get_padded_string label value out_var) + set(pad_string "........................................") + string(LENGTH "${label}" label_len) + string(LENGTH "${pad_string}" pad_len) + math(EXPR pad_len "${pad_len}-${label_len}") + if(pad_len LESS "0") + set(pad_len "0") + endif() + string(SUBSTRING "${pad_string}" 0 "${pad_len}" pad_string) + set(output "${label} ${pad_string} ${value}") + set("${out_var}" "${output}" PARENT_SCOPE) +endfunction() + +function(qt_configure_add_summary_entry) + qt_configure_record_command(ADD_SUMMARY_ENTRY ${ARGV}) +endfunction() + +function(qt_configure_process_add_summary_entry) + qt_parse_all_arguments(arg "qt_configure_add_summary_entry" + "" + "ARGS;TYPE;MESSAGE" "CONDITION" ${ARGN}) + + if(NOT arg_TYPE) + set(arg_TYPE "feature") + endif() + + if(NOT "${arg_CONDITION}" STREQUAL "") + qt_evaluate_config_expression(condition_result ${arg_CONDITION}) + if(NOT condition_result) + return() + endif() + endif() + + if(arg_TYPE STREQUAL "firstAvailableFeature") + set(first_feature_found FALSE) + set(message "") + string(REPLACE " " ";" args_list "${arg_ARGS}") + foreach(feature ${args_list}) + qt_feature_normalize_name("${feature}" feature) + if(NOT DEFINED QT_FEATURE_${feature}) + message(FATAL_ERROR "Asking for a report on undefined feature ${feature}.") + endif() + if(QT_FEATURE_${feature}) + set(first_feature_found TRUE) + set(message "${QT_FEATURE_LABEL_${feature}}") + break() + endif() + endforeach() + + if(NOT first_feature_found) + set(message "") + endif() + qt_configure_add_report_padded("${arg_MESSAGE}" "${message}") + elseif(arg_TYPE STREQUAL "featureList") + set(available_features "") + string(REPLACE " " ";" args_list "${arg_ARGS}") + + foreach(feature ${args_list}) + qt_feature_normalize_name("${feature}" feature) + if(NOT DEFINED QT_FEATURE_${feature}) + message(FATAL_ERROR "Asking for a report on undefined feature ${feature}.") + endif() + if(QT_FEATURE_${feature}) + list(APPEND available_features "${QT_FEATURE_LABEL_${feature}}") + endif() + endforeach() + + if(NOT available_features) + set(message "") + else() + list(JOIN available_features " " message) + endif() + qt_configure_add_report_padded("${arg_MESSAGE}" "${message}") + elseif(arg_TYPE STREQUAL "feature") + qt_feature_normalize_name("${arg_ARGS}" feature) + + set(label "${QT_FEATURE_LABEL_${feature}}") + + if(NOT label) + set(label "${feature}") + endif() + + if(QT_FEATURE_${feature}) + set(value "yes") + else() + set(value "no") + endif() + + qt_configure_add_report_padded("${label}" "${value}") + endif() +endfunction() + +function(qt_configure_add_summary_build_type_and_config) + qt_configure_record_command(ADD_BUILD_TYPE_AND_CONFIG ${ARGV}) +endfunction() + +function(qt_configure_process_add_summary_build_type_and_config) + get_property(subarch_summary GLOBAL PROPERTY qt_configure_subarch_summary) + set(message + "Building for: ${QT_QMAKE_TARGET_MKSPEC} (${TEST_architecture_arch}, CPU features: ${subarch_summary})") + qt_configure_add_report("${message}") + + set(message "Compiler: ") + if(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + string(APPEND message "clang (Apple)") + elseif(CLANG) + string(APPEND message "clang") + elseif(ICC) + string(APPEND message "intel_icc") + elseif(QCC) + string(APPEND message "rim_qcc") + elseif(GCC) + string(APPEND message "gcc") + elseif(MSVC) + string(APPEND message "msvc") + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GHS") + string(APPEND message "ghs") + else() + string(APPEND message "unknown (${CMAKE_CXX_COMPILER_ID})") + endif() + string(APPEND message " ${CMAKE_CXX_COMPILER_VERSION}") + qt_configure_add_report("${message}") +endfunction() + +function(qt_configure_add_summary_build_mode) + qt_configure_record_command(ADD_BUILD_MODE ${ARGV}) +endfunction() + +function(qt_configure_process_add_summary_build_mode label) + set(message "") + if(DEFINED CMAKE_BUILD_TYPE) + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + string(APPEND message "debug") + elseif(CMAKE_BUILD_TYPE STREQUAL "Release") + string(APPEND message "release") + elseif(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + string(APPEND message "release (with debug info)") + else() + string(APPEND message "${CMAKE_BUILD_TYPE}") + endif() + elseif(DEFINED CMAKE_CONFIGURATION_TYPES) + if("Release" IN_LIST CMAKE_CONFIGURATION_TYPES + AND "Debug" IN_LIST CMAKE_CONFIGURATION_TYPES) + string(APPEND message "debug and release") + elseif("RelWithDebInfo" IN_LIST CMAKE_CONFIGURATION_TYPES + AND "Debug" IN_LIST CMAKE_CONFIGURATION_TYPES) + string(APPEND message "debug and release (with debug info)") + else() + string(APPEND message "${CMAKE_CONFIGURATION_TYPES}") + endif() + endif() + + qt_configure_add_report_padded("${label}" "${message}") +endfunction() + +function(qt_configure_add_summary_section) + qt_configure_record_command(ADD_SUMMARY_SECTION ${ARGV}) +endfunction() + +function(qt_configure_process_add_summary_section) + qt_parse_all_arguments(arg "qt_configure_add_summary_section" + "" "NAME" "" ${ARGN}) + + qt_configure_add_report("${__qt_configure_indent}${arg_NAME}:") + if(NOT DEFINED __qt_configure_indent) + set(__qt_configure_indent " " PARENT_SCOPE) + else() + set(__qt_configure_indent "${__qt_configure_indent} " PARENT_SCOPE) + endif() +endfunction() + +function(qt_configure_end_summary_section) + qt_configure_record_command(END_SUMMARY_SECTION ${ARGV}) +endfunction() + +function(qt_configure_process_end_summary_section) + string(LENGTH "${__qt_configure_indent}" indent_len) + if(indent_len GREATER_EQUAL 2) + string(SUBSTRING "${__qt_configure_indent}" 2 -1 __qt_configure_indent) + set(__qt_configure_indent "${__qt_configure_indent}" PARENT_SCOPE) + endif() +endfunction() + +function(qt_configure_add_report_entry) + qt_configure_record_command(ADD_REPORT_ENTRY ${ARGV}) +endfunction() + +function(qt_configure_process_add_report_entry) + qt_parse_all_arguments(arg "qt_configure_add_report_entry" + "" + "TYPE;MESSAGE" "CONDITION" ${ARGN}) + + set(possible_types NOTE WARNING ERROR FATAL_ERROR) + if(NOT "${arg_TYPE}" IN_LIST possible_types) + message(FATAL_ERROR "qt_configure_add_report_entry: '${arg_TYPE}' is not a valid type.") + endif() + + if(NOT arg_MESSAGE) + message(FATAL_ERROR "qt_configure_add_report_entry: Empty message given.") + endif() + + if(arg_TYPE STREQUAL "NOTE") + set(contents_var "__qt_configure_notes") + set(prefix "Note: ") + elseif(arg_TYPE STREQUAL "WARNING") + set(contents_var "__qt_configure_warnings") + set(prefix "WARNING: ") + elseif(arg_TYPE STREQUAL "ERROR") + set(contents_var "__qt_configure_errors") + set(prefix "ERROR: ") + elseif(arg_TYPE STREQUAL "FATAL_ERROR") + set(contents_var "__qt_configure_errors") + set(prefix "FATAL ERROR: ") + endif() + + if(NOT "${arg_CONDITION}" STREQUAL "") + qt_evaluate_config_expression(condition_result ${arg_CONDITION}) + endif() + + if("${arg_CONDITION}" STREQUAL "" OR condition_result) + set(new_report "${prefix}${arg_MESSAGE}") + string(APPEND "${contents_var}" "\n${new_report}") + + if(arg_TYPE STREQUAL "ERROR" OR arg_TYPE STREQUAL "FATAL_ERROR") + set(__qt_configure_an_error_occurred "TRUE" PARENT_SCOPE) + endif() + endif() + + set("${contents_var}" "${${contents_var}}" PARENT_SCOPE) +endfunction() diff --git a/cmake/QtBuildInternals/QtBuildInternalsConfig.cmake b/cmake/QtBuildInternals/QtBuildInternalsConfig.cmake index b44ec7f435..c945fd124a 100644 --- a/cmake/QtBuildInternals/QtBuildInternalsConfig.cmake +++ b/cmake/QtBuildInternals/QtBuildInternalsConfig.cmake @@ -94,8 +94,6 @@ macro(qt_build_repo_begin) endmacro() macro(qt_build_repo_end) - include(QtBuildInformation) - if(NOT QT_BUILD_STANDALONE_TESTS) # Delayed actions on some of the Qt targets: include(QtPostProcess) diff --git a/cmake/QtFeature.cmake b/cmake/QtFeature.cmake index 7b852b3c2e..34ae83b55d 100644 --- a/cmake/QtFeature.cmake +++ b/cmake/QtFeature.cmake @@ -14,6 +14,9 @@ function(qt_feature_module_begin) if ("${arg_PRIVATE_FILE}" STREQUAL "") message(FATAL_ERROR "qt_feature_begin_module needs a PRIVATE_FILE name!") endif() + set(__QtFeature_only_evaluate_features OFF PARENT_SCOPE) + else() + set(__QtFeature_only_evaluate_features ON PARENT_SCOPE) endif() set(__QtFeature_library "${arg_LIBRARY}" PARENT_SCOPE) @@ -278,6 +281,9 @@ function(qt_evaluate_feature feature) qt_feature_set_cache_value(cache "${feature}" "${emit_if}" "${result}" "${arg_LABEL}") qt_feature_set_value("${feature}" "${cache}" "${emit_if}" "${condition}" "${arg_LABEL}") + + # Store each feature's label for summary info. + set(QT_FEATURE_LABEL_${feature} "${arg_LABEL}" CACHE INTERNAL "") endfunction() function(qt_feature_config feature config_var_name) @@ -581,6 +587,7 @@ function(qt_feature_module_end) unset(__QtFeature_define_definitions PARENT_SCOPE) unset(__QtFeature_custom_enabled_features PARENT_SCOPE) unset(__QtFeature_custom_disabled_features PARENT_SCOPE) + unset(__QtFeature_only_evaluate_features PARENT_SCOPE) endfunction() function(qt_feature_copy_global_config_features_to_core target) diff --git a/cmake/QtSetup.cmake b/cmake/QtSetup.cmake index 4b5e632847..e8f1caf2fa 100644 --- a/cmake/QtSetup.cmake +++ b/cmake/QtSetup.cmake @@ -148,6 +148,7 @@ endif() include(QtBuild) ## Qt Feature support: +include(QtBuildInformation) include(QtFeature) ## Compiler optimization flags: diff --git a/util/cmake/configurejson2cmake.py b/util/cmake/configurejson2cmake.py index ad183c1906..390042760b 100755 --- a/util/cmake/configurejson2cmake.py +++ b/util/cmake/configurejson2cmake.py @@ -255,7 +255,7 @@ def map_condition(condition): mapped_features = {"gbm": "gbm_FOUND", "system-xcb": "ON"} # Turn foo != "bar" into (NOT foo STREQUAL 'bar') - condition = re.sub(r"(.+)\s*!=\s*('.+')", "(! \\1 == \\2)", condition) + condition = re.sub(r"([^ ]+)\s*!=\s*('.*?')", "(! \\1 == \\2)", condition) condition = condition.replace("!", "NOT ") condition = condition.replace("&&", " AND ") @@ -753,7 +753,7 @@ def parseTest(ctx, test, data, cm_fh): print(f" XXXX UNHANDLED TEST TYPE {data['type']} in test description") -def parseFeature(ctx, feature, data, cm_fh): +def get_feature_mapping(): # This is *before* the feature name gets normalized! So keep - and + chars, etc. feature_mapping = { "alloc_h": None, # handled by alloc target @@ -848,7 +848,11 @@ def parseFeature(ctx, feature, data, cm_fh): "webp": {"condition": "QT_FEATURE_imageformatplugin AND WrapWebP_FOUND"}, "xkbcommon-system": None, # another system library, just named a bit different from the rest } + return feature_mapping + +def parseFeature(ctx, feature, data, cm_fh): + feature_mapping = get_feature_mapping() mapping = feature_mapping.get(feature, {}) if mapping is None: @@ -1087,6 +1091,110 @@ def parseFeature(ctx, feature, data, cm_fh): cm_fh.write(")\n") +def processSummaryHelper(ctx, entries, cm_fh): + for entry in entries: + if isinstance(entry, str): + name = entry + cm_fh.write(f'qt_configure_add_summary_entry(ARGS "{name}")\n') + elif "type" in entry \ + and entry["type"] in ["feature", "firstAvailableFeature", "featureList"]: + function_args = [] + entry_type = entry["type"] + + if entry_type in ["firstAvailableFeature", "featureList"]: + feature_mapping = get_feature_mapping() + unhandled_feature = False + for feature_name, value in feature_mapping.items(): + # Skip entries that mention a feature which is + # skipped by configurejson2cmake in the feature + # mapping. This is not ideal, but prevents errors at + # CMake configuration time. + if not value and f"{feature_name}" in entry["args"]: + unhandled_feature = True + break + + if unhandled_feature: + print(f" XXXX UNHANDLED FEATURE in SUMMARY TYPE {entry}.") + continue + + if entry_type != "feature": + function_args.append(lineify("TYPE", entry_type)) + if "args" in entry: + args = entry["args"] + function_args.append(lineify("ARGS", args)) + if "message" in entry: + message = entry["message"] + function_args.append(lineify("MESSAGE", message)) + if "condition" in entry: + condition = map_condition(entry["condition"]) + function_args.append(lineify("CONDITION", condition, quote=False)) + entry_args_string = "".join(function_args) + cm_fh.write(f'qt_configure_add_summary_entry(\n{entry_args_string})\n') + elif "type" in entry and entry["type"] == "buildTypeAndConfig": + cm_fh.write(f'qt_configure_add_summary_build_type_and_config()\n') + elif "type" in entry and entry["type"] == "buildMode": + message = entry["message"] + cm_fh.write(f'qt_configure_add_summary_build_mode({message})\n') + elif "section" in entry: + section = entry["section"] + cm_fh.write(f'qt_configure_add_summary_section(NAME "{section}")\n') + processSummaryHelper(ctx, entry["entries"], cm_fh) + cm_fh.write(f'qt_configure_end_summary_section() # end of "{section}" section\n') + else: + print(f" XXXX UNHANDLED SUMMARY TYPE {entry}.") + + +def processReportHelper(ctx, entries, cm_fh): + feature_mapping = get_feature_mapping() + + for entry in entries: + if isinstance(entry, dict): + entry_args = [] + if "type" not in entry: + print(f" XXXX UNHANDLED REPORT TYPE missing type in {entry}.") + continue + + report_type = entry["type"] + if report_type not in ["note", "warning", "error"]: + print(f" XXXX UNHANDLED REPORT TYPE unknown type in {entry}.") + continue + + report_type = report_type.upper() + entry_args.append(lineify("TYPE", report_type, quote=False)) + message = entry["message"] + + # Replace semicolons, qt_parse_all_arguments can't handle + # them due to an escaping bug in CMake regarding escaping + # macro arguments. + # https://gitlab.kitware.com/cmake/cmake/issues/19972 + message = message.replace(";", ",") + + entry_args.append(lineify("MESSAGE", message)) + # Need to overhaul everything to fix conditions. + if "condition" in entry: + condition = entry["condition"] + + unhandled_condition = False + for feature_name, value in feature_mapping.items(): + # Skip reports that mention a feature which is + # skipped by configurejson2cmake in the feature + # mapping. This is not ideal, but prevents errors at + # CMake configuration time. + if not value and f"features.{feature_name}" in condition: + unhandled_condition = True + break + + if unhandled_condition: + print(f" XXXX UNHANDLED CONDITION in REPORT TYPE {entry}.") + continue + condition = map_condition(condition) + entry_args.append(lineify("CONDITION", condition, quote=False)) + entry_args_string = "".join(entry_args) + cm_fh.write(f'qt_configure_add_report_entry(\n{entry_args_string})\n') + else: + print(f" XXXX UNHANDLED REPORT TYPE {entry}.") + + def processInputs(ctx, data, cm_fh): print(" inputs:") if "commandline" not in data: @@ -1128,6 +1236,18 @@ def processLibraries(ctx, data, cm_fh): parseLib(ctx, lib, data, cm_fh, cmake_find_packages_set) +def processReports(ctx, data, cm_fh): + if "summary" in data: + print(" summary:") + processSummaryHelper(ctx, data["summary"], cm_fh) + if "report" in data: + print(" report:") + processReportHelper(ctx, data["report"], cm_fh) + if "earlyReport" in data: + print(" earlyReport:") + processReportHelper(ctx, data["earlyReport"], cm_fh) + + def processSubconfigs(path, ctx, data): assert ctx is not None if "subconfigs" in data: @@ -1162,6 +1282,8 @@ def processJson(path, ctx, data): processFeatures(ctx, data, cm_fh) + processReports(ctx, data, cm_fh) + if ctx.get("module") == "global": cm_fh.write( '\nqt_extra_definition("QT_VERSION_STR" "\\"${PROJECT_VERSION}\\"" PUBLIC)\n' -- cgit v1.2.3