# Copyright (C) 2024 The Qt Company Ltd. # SPDX-License-Identifier: BSD-3-Clause macro(qt_tl_include_all_helpers) include(QtIRHelpers) qt_ir_include_all_helpers() endmacro() function(qt_tl_run_toplevel_configure top_level_src_path) cmake_parse_arguments(arg "ALREADY_INITIALIZED" "" "" ${ARGV}) qt_ir_get_cmake_flag(ALREADY_INITIALIZED arg_ALREADY_INITIALIZED) # Filter out init-repository specific arguments before passing them to # configure. qt_ir_get_args_from_optfile_configure_filtered("${OPTFILE}" configure_args ${arg_ALREADY_INITIALIZED}) # Get the path to the qtbase configure script. set(qtbase_dir_name "qtbase") set(configure_path "${top_level_src_path}/${qtbase_dir_name}/configure") if(CMAKE_HOST_WIN32) string(APPEND configure_path ".bat") endif() if(NOT EXISTS "${configure_path}") message(FATAL_ERROR "The required qtbase/configure script was not found: ${configure_path}\n" "Try re-running configure with --init-submodules") endif() # Make a build directory for qtbase in the current build directory. set(qtbase_build_dir "${CMAKE_CURRENT_BINARY_DIR}/${qtbase_dir_name}") file(MAKE_DIRECTORY "${qtbase_build_dir}") qt_ir_execute_process_and_log_and_handle_error( COMMAND_ARGS "${configure_path}" -top-level ${configure_args} WORKING_DIRECTORY "${qtbase_build_dir}" FORCE_VERBOSE ) endfunction() function(qt_tl_run_main_script) if(NOT TOP_LEVEL_SRC_PATH) message(FATAL_ERROR "Assertion: configure TOP_LEVEL_SRC_PATH is not set") endif() # Tell init-repository it is called from configure. qt_ir_set_option_value(from-configure TRUE) # Run init-repository in-process. qt_ir_run_main_script("${TOP_LEVEL_SRC_PATH}" exit_reason) if(exit_reason AND NOT exit_reason STREQUAL "ALREADY_INITIALIZED") return() endif() # Then run configure out-of-process. qt_tl_run_toplevel_configure("${TOP_LEVEL_SRC_PATH}" ${exit_reason}) endfunction() # Populates $out_module_list with all subdirectories that have a CMakeLists.txt file function(qt_internal_find_modules out_module_list) set(module_list "") file(GLOB directories LIST_DIRECTORIES true RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" *) foreach(directory IN LISTS directories) if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${directory}" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${directory}/CMakeLists.txt") list(APPEND module_list "${directory}") endif() endforeach() message(DEBUG "qt_internal_find_modules: ${module_list}") set(${out_module_list} "${module_list}" PARENT_SCOPE) endfunction() # poor man's yaml parser, populating $out_dependencies with all dependencies # in the $depends_file # Each entry will be in the format dependency/sha1/required function(qt_internal_parse_dependencies_yaml depends_file out_dependencies) file(STRINGS "${depends_file}" lines) set(eof_marker "---EOF---") list(APPEND lines "${eof_marker}") set(required_default TRUE) set(dependencies "") set(dependency "") set(revision "") set(required "${required_default}") foreach(line IN LISTS lines) if(line MATCHES "^ (.+):$" OR line STREQUAL "${eof_marker}") # Found a repo entry or end of file. Add the last seen dependency. if(NOT dependency STREQUAL "") if(revision STREQUAL "") message(FATAL_ERROR "Format error in ${depends_file} - ${dependency} does not specify revision!") endif() list(APPEND dependencies "${dependency}/${revision}/${required}") endif() # Remember the current dependency if(NOT line STREQUAL "${eof_marker}") set(dependency "${CMAKE_MATCH_1}") set(revision "") set(required "${required_default}") # dependencies are specified with relative path to this module string(REPLACE "../" "" dependency ${dependency}) endif() elseif(line MATCHES "^ ref: (.+)$") set(revision "${CMAKE_MATCH_1}") elseif(line MATCHES "^ required: (.+)$") string(TOUPPER "${CMAKE_MATCH_1}" required) endif() endforeach() message(DEBUG "qt_internal_parse_dependencies_yaml for ${depends_file}\n dependencies: ${dependencies}") set(${out_dependencies} "${dependencies}" PARENT_SCOPE) endfunction() # Helper macro for qt_internal_resolve_module_dependencies. macro(qt_internal_resolve_module_dependencies_set_skipped value) if(DEFINED arg_SKIPPED_VAR) set(${arg_SKIPPED_VAR} ${value} PARENT_SCOPE) endif() endmacro() # Strips tqtc- prefix from a repo name. function(qt_internal_normalize_repo_name repo_name out_var) string(REGEX REPLACE "^tqtc-" "" normalized "${repo_name}") set(${out_var} "${normalized}" PARENT_SCOPE) endfunction() # Checks if a directory with the given repo name exists in the current # source / working directory. If it doesn't, it strips the tqtc- prefix. function(qt_internal_use_normalized_repo_name_if_needed repo_name out_var) set(base_dir "${CMAKE_CURRENT_SOURCE_DIR}") set(repo_dir "${base_dir}/${repo_name}") if(NOT IS_DIRECTORY "${repo_dir}") qt_internal_normalize_repo_name("${repo_name}" repo_name) endif() set(${out_var} "${repo_name}" PARENT_SCOPE) endfunction() # Resolve the dependencies of the given module. # "Module" in the sense of Qt repository. # # Side effects: Sets the global properties QT_DEPS_FOR_${module} and QT_REQUIRED_DEPS_FOR_${module} # with the direct (required) dependencies of module. # # # Positional arguments: # # module is the Qt repository. # # out_ordered is where the result is stored. This is a list of all dependencies, including # transitive ones, in topologically sorted order. Note that ${module} itself is also part of # out_ordered. # # out_revisions is a list of git commit IDs for each of the dependencies in ${out_ordered}. This # list has the same length as ${out_ordered}. # # # Keyword arguments: # # PARSED_DEPENDENCIES is a list of dependencies of module in the format that # qt_internal_parse_dependencies_yaml returns. # If this argument is not provided, either a module's dependencies.yaml or .gitmodules file is # used as the source of dependencies, depending on whether PARSE_GITMODULES option is enabled. # # PARSE_GITMODULES is a boolean that controls whether the .gitmodules or the dependencies.yaml # file of the repo are used for extracting dependencies. Defaults to FALSE, so uses # dependencies.yaml by default. # # EXCLUDE_OPTIONAL_DEPS is a boolean that controls whether optional dependencies are excluded from # the final result. # # GITMODULES_PREFIX_VAR is the prefix of all the variables containing dependencies for the # PARSE_GITMODULES mode. # The function expects the following variables to be set in the parent scope # ${arg_GITMODULES_PREFIX_VAR}_${submodule_name}_depends # ${arg_GITMODULES_PREFIX_VAR}_${submodule_name}_recommends # # IN_RECURSION is an internal option that is set when the function is in recursion. # # REVISION is an internal value with the git commit ID that belongs to ${module}. # # SKIPPED_VAR is an output variable name that is set to TRUE if the module was skipped, to FALSE # otherwise. # # NORMALIZE_REPO_NAME_IF_NEEDED Will remove 'tqtc-' from the beginning of submodule dependencies # if a tqtc- named directory does not exist. # # SKIP_MODULES Modules that should be skipped from evaluation completely. function(qt_internal_resolve_module_dependencies module out_ordered out_revisions) set(options IN_RECURSION NORMALIZE_REPO_NAME_IF_NEEDED PARSE_GITMODULES EXCLUDE_OPTIONAL_DEPS) set(oneValueArgs REVISION SKIPPED_VAR GITMODULES_PREFIX_VAR) set(multiValueArgs PARSED_DEPENDENCIES SKIP_MODULES) cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) # Clear the property that stores the repositories we've already seen. if(NOT arg_IN_RECURSION) set_property(GLOBAL PROPERTY _qt_internal_seen_repos) endif() # Bail out if we've seen the module already or it was skipped explicitly from command line. qt_internal_resolve_module_dependencies_set_skipped(FALSE) get_property(seen GLOBAL PROPERTY _qt_internal_seen_repos) if(module IN_LIST seen OR module IN_LIST arg_SKIP_MODULES) qt_internal_resolve_module_dependencies_set_skipped(TRUE) return() endif() set_property(GLOBAL APPEND PROPERTY _qt_internal_seen_repos ${module}) # Set a default REVISION. if("${arg_REVISION}" STREQUAL "") set(arg_REVISION HEAD) endif() # Retrieve the dependencies. if(DEFINED arg_PARSED_DEPENDENCIES) set(dependencies "${arg_PARSED_DEPENDENCIES}") else() set(dependencies "") if(NOT arg_PARSE_GITMODULES) set(depends_file "${CMAKE_CURRENT_SOURCE_DIR}/${module}/dependencies.yaml") if(EXISTS "${depends_file}") qt_internal_parse_dependencies_yaml("${depends_file}" dependencies) if(arg_EXCLUDE_OPTIONAL_DEPS) set(filtered_dependencies "") foreach(dependency IN LISTS dependencies) string(REPLACE "/" ";" dependency_split "${dependency}") list(GET dependency_split 2 required) if(required) list(APPEND filtered_dependencies "${dependency}") endif() endforeach() set(dependencies "${filtered_dependencies}") endif() endif() else() set(depends "${${arg_GITMODULES_PREFIX_VAR}_${dependency}_depends}") foreach(dependency IN LISTS depends) if(dependency) # The HEAD value is not really used, but we need to add something. list(APPEND dependencies "${dependency}/HEAD/TRUE") endif() endforeach() set(recommends "${${arg_GITMODULES_PREFIX_VAR}_${dependency}_recommends}") if(NOT arg_EXCLUDE_OPTIONAL_DEPS) foreach(dependency IN LISTS recommends) if(dependency) list(APPEND dependencies "${dependency}/HEAD/FALSE") endif() endforeach() endif() endif() endif() # Traverse the dependencies. set(ordered) set(revisions) foreach(dependency IN LISTS dependencies) if(dependency MATCHES "(.*)/([^/]+)/([^/]+)") set(dependency "${CMAKE_MATCH_1}") set(revision "${CMAKE_MATCH_2}") set(required "${CMAKE_MATCH_3}") else() message(FATAL_ERROR "Internal Error: wrong dependency format ${dependency}") endif() set(normalize_arg "") if(arg_NORMALIZE_REPO_NAME_IF_NEEDED) qt_internal_use_normalized_repo_name_if_needed("${dependency}" dependency) set(normalize_arg "NORMALIZE_REPO_NAME_IF_NEEDED") endif() set_property(GLOBAL APPEND PROPERTY QT_DEPS_FOR_${module} ${dependency}) if(required) set_property(GLOBAL APPEND PROPERTY QT_REQUIRED_DEPS_FOR_${module} ${dependency}) endif() set(parse_gitmodules "") if(arg_PARSE_GITMODULES) set(parse_gitmodules "PARSE_GITMODULES") endif() set(exclude_optional_deps "") if(arg_EXCLUDE_OPTIONAL_DEPS) set(exclude_optional_deps "EXCLUDE_OPTIONAL_DEPS") endif() set(extra_options "") if(arg_SKIP_MODULES) list(APPEND extra_options SKIP_MODULES ${arg_SKIP_MODULES}) endif() qt_internal_resolve_module_dependencies(${dependency} dep_ordered dep_revisions REVISION "${revision}" SKIPPED_VAR skipped IN_RECURSION ${normalize_arg} ${parse_gitmodules} ${exclude_optional_deps} GITMODULES_PREFIX_VAR ${arg_GITMODULES_PREFIX_VAR} ${extra_options} ) if(NOT skipped) list(APPEND ordered ${dep_ordered}) list(APPEND revisions ${dep_revisions}) endif() endforeach() list(APPEND ordered ${module}) list(APPEND revisions ${arg_REVISION}) set(${out_ordered} "${ordered}" PARENT_SCOPE) set(${out_revisions} "${revisions}" PARENT_SCOPE) endfunction() # Resolves the dependencies of the given modules. # "Module" is here used in the sense of Qt repository. # # Returns all dependencies, including transitive ones, in topologically sorted order. # # Arguments: # modules is the initial list of repos. # out_all_ordered is the variable name where the result is stored. # PARSE_GITMODULES and GITMODULES_PREFIX_VAR are keyowrd arguments that change the # source of dependencies parsing from dependencies.yaml to .gitmodules. # EXCLUDE_OPTIONAL_DEPS is a keyword argument that excludes optional dependencies from the result. # See qt_internal_resolve_module_dependencies for details. # # SKIP_MODULES Modules that should be skipped from evaluation completely. # # See qt_internal_resolve_module_dependencies for side effects. function(qt_internal_sort_module_dependencies modules out_all_ordered) set(options PARSE_GITMODULES EXCLUDE_OPTIONAL_DEPS) set(oneValueArgs GITMODULES_PREFIX_VAR) set(multiValueArgs SKIP_MODULES) cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) set(parse_gitmodules "") if(arg_PARSE_GITMODULES) set(parse_gitmodules "PARSE_GITMODULES") endif() set(exclude_optional_deps "") if(arg_EXCLUDE_OPTIONAL_DEPS) set(exclude_optional_deps "EXCLUDE_OPTIONAL_DEPS") endif() # Create a fake repository "all_selected_repos" that has all repositories from the input as # required dependency. The format must match what qt_internal_parse_dependencies_yaml produces. set(all_selected_repos_as_parsed_dependencies) foreach(module IN LISTS modules) list(APPEND all_selected_repos_as_parsed_dependencies "${module}/HEAD/FALSE") endforeach() set(extra_args "") if(arg_SKIP_MODULES) set(extra_args SKIP_MODULES ${arg_SKIP_MODULES}) endif() qt_internal_resolve_module_dependencies(all_selected_repos ordered unused_revisions PARSED_DEPENDENCIES ${all_selected_repos_as_parsed_dependencies} NORMALIZE_REPO_NAME_IF_NEEDED ${exclude_optional_deps} ${parse_gitmodules} GITMODULES_PREFIX_VAR ${arg_GITMODULES_PREFIX_VAR} ${extra_args} ) # Drop "all_selected_repos" from the output. It depends on all selected repos, thus it must be # the last element in the topologically sorted list. list(REMOVE_AT ordered -1) message(DEBUG "qt_internal_sort_module_dependencies input modules: ${modules}\n topo-sorted: ${ordered}") set(${out_all_ordered} "${ordered}" PARENT_SCOPE) endfunction() # Checks whether any unparsed arguments have been passed to the function at the call site. # Use this right after `cmake_parse_arguments`. function(qt_internal_tl_validate_all_args_are_parsed prefix) if(DEFINED ${prefix}_UNPARSED_ARGUMENTS) message(FATAL_ERROR "Unknown arguments: (${${prefix}_UNPARSED_ARGUMENTS})") endif() endfunction() # If VERBOSE is not set or FALSE in the parent or root scopes, swallow the git output. # If VERBOSE is true, echo the stdout output, as well as the command run. function(qt_internal_tl_handle_verbose_git_operations) set(swallow_output "") # unless VERBOSE, eat git output, show it in case of error if (NOT VERBOSE) list(APPEND swallow_output "OUTPUT_VARIABLE" "git_output" "ERROR_VARIABLE" "git_output") else() list(APPEND swallow_output COMMAND_ECHO STDOUT) endif() set(swallow_output "${swallow_output}" PARENT_SCOPE) endfunction() # Returns true if the current working directory is a super module with a .gitmodules file. # Likely means it's the qt5.git super repo. function(qt_internal_tl_is_super_repo out_var) execute_process( COMMAND "git" "rev-parse" "--show-toplevel" RESULT_VARIABLE git_result OUTPUT_VARIABLE top_level_path ERROR_VARIABLE git_stderr OUTPUT_STRIP_TRAILING_WHITESPACE ) if(NOT git_result AND top_level_path AND EXISTS "${top_level_path}/.gitmodules") set(result TRUE) else() set(result FALSE) endif() set(${out_var} ${result} PARENT_SCOPE) endfunction() # Returns whether the given git repo is shallow (cloned with --depth arg). function(qt_internal_tl_is_git_repo_shallow out_var working_directory) message(DEBUG "Checking if repo in '${working_directory}' is shallow") execute_process( COMMAND "git" "rev-parse" "--is-shallow-repository" RESULT_VARIABLE git_result OUTPUT_VARIABLE git_stdout ERROR_VARIABLE git_stderr WORKING_DIRECTORY "${working_directory}" OUTPUT_STRIP_TRAILING_WHITESPACE ) if(git_result) message(FATAL_ERROR "Failed to check if repo is shallow in '${working_directory}'\n" "stdout: ${git_stdout}\n" "stderr: ${git_stderr}") endif() string(STRIP "${git_stdout}" git_stdout) if(git_stdout) set(value TRUE) else() set(value FALSE) endif() set(${out_var} "${value}" PARENT_SCOPE) endfunction() # Returns whether the given refspec is known to the repo in the given working directory. function(qt_internal_tl_is_git_ref_spec_known out_var refspec working_directory) # The funny ^{commit} syntax means the refpsec resolves to a commit, as opposed to a blob or a # tree. execute_process( COMMAND "git" "cat-file" "-e" "${refspec}^{commit}" RESULT_VARIABLE git_result OUTPUT_VARIABLE git_stdout ERROR_VARIABLE git_stderr WORKING_DIRECTORY "${working_directory}" OUTPUT_STRIP_TRAILING_WHITESPACE ) # A non-0 exit code means it doesn't exist. if(git_result) set(value FALSE) else() set(value TRUE) endif() set(${out_var} "${value}" PARENT_SCOPE) endfunction() # Unshallow the given git repo. function(qt_internal_tl_git_unshallow_repo) set(opt_args SHOW_PROGRESS ) set(single_args WORKING_DIRECTORY ) set(multi_args "") cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") qt_internal_tl_validate_all_args_are_parsed(arg) if(NOT arg_WORKING_DIRECTORY) message(FATAL_ERROR "WORKING_DIRECTORY is required") endif() set(args "") if(arg_SHOW_PROGRESS) list(APPEND args --progress) endif() execute_process( COMMAND "git" "fetch" "--unshallow" ${args} RESULT_VARIABLE git_result WORKING_DIRECTORY "${arg_WORKING_DIRECTORY}" ${swallow_output} ) if(git_result) message(FATAL_ERROR "Failed to unshallow repo in '${arg_WORKING_DIRECTORY}': ${git_stderr}") endif() endfunction() # Fetches a shallow ref spec (--depth 1) from the given remote. function(qt_internal_tl_git_fetch_shallow_ref_spec) set(opt_args SHOW_PROGRESS FATAL ) set(single_args REMOTE REF_SPEC WORKING_DIRECTORY OUT_VAR_RESULT ) set(multi_args "") cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") qt_internal_tl_validate_all_args_are_parsed(arg) if(NOT arg_REF_SPEC) message(FATAL_ERROR "REF_SPEC is required") endif() if(NOT arg_REMOTE) message(FATAL_ERROR "REMOTE is required") endif() if(NOT arg_WORKING_DIRECTORY) message(FATAL_ERROR "WORKING_DIRECTORY is required") endif() if(NOT arg_OUT_VAR_RESULT) message(FATAL_ERROR "OUT_VAR_RESULT is required") endif() set(args "") if(arg_SHOW_PROGRESS) list(APPEND args --progress) endif() execute_process( COMMAND "git" "fetch" "--depth" "1" "${arg_REMOTE}" "${arg_REF_SPEC}" ${args} RESULT_VARIABLE git_result WORKING_DIRECTORY "${arg_WORKING_DIRECTORY}" ${swallow_output} ) if(git_result) set(result FALSE) if(arg_FATAL) set(message_type FATAL_ERROR) else() set(message_type DEBUG) endif() message(${message_type} "Failed to fetch shallow ref spec '${arg_REF_SPEC}' " "in '${arg_WORKING_DIRECTORY}': ${git_stderr}") else() set(result TRUE) endif() if(arg_OUT_VAR_RESULT) set("${arg_OUT_VAR_RESULT}" "${result}" PARENT_SCOPE) endif() endfunction() # Detects if a repo is shallow. If it is, checks if the given ref spec is known. If not, it will # try to fetch it. If it's still unknown, will try to unshallow the repo. function(qt_internal_tl_handle_shallow_repo) set(opt_args SHOW_PROGRESS ) set(single_args REF_SPEC REMOTE_NAME WORKING_DIRECTORY ) set(multi_args "") cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") qt_internal_tl_validate_all_args_are_parsed(arg) if(NOT arg_REF_SPEC) message(FATAL_ERROR "REF_SPEC is required") endif() if(NOT arg_REMOTE_NAME) message(FATAL_ERROR "REMOTE_NAME is required") endif() if(NOT arg_WORKING_DIRECTORY) message(FATAL_ERROR "WORKING_DIRECTORY is required") endif() qt_internal_tl_is_git_repo_shallow(is_shallow "${arg_WORKING_DIRECTORY}") if(NOT is_shallow) return() endif() qt_internal_tl_is_git_ref_spec_known(is_known "${arg_REF_SPEC}" "${arg_WORKING_DIRECTORY}") if(is_known) return() endif() set(remote "${arg_REMOTE_NAME}") message(DEBUG "Fetching with --depth 1 from '${remote}' due to unknown refspec '${arg_REF_SPEC}'") set(args "") if(arg_SHOW_PROGRESS) list(APPEND args SHOW_PROGRESS) endif() qt_internal_tl_git_fetch_shallow_ref_spec( REF_SPEC "${arg_REF_SPEC}" REMOTE "${remote}" WORKING_DIRECTORY "${arg_WORKING_DIRECTORY}" OUT_VAR_RESULT shallow_fetch_succeeded ${args} ) # Attempt to unshallow a repo if a ref spec that we are meant to check out to, # is not known. if(shallow_fetch_succeeded) return() endif() message(DEBUG "Unshallowing repo in ${arg_WORKING_DIRECTORY} due to unknown " "refspec ${arg_REF_SPEC}") qt_internal_tl_git_unshallow_repo( WORKING_DIRECTORY "${arg_WORKING_DIRECTORY}" ${args} ) endfunction() # Checks if given string looks like a sha1. function(qt_internal_tl_is_sha1_ish value out_var) set(hex_digit "[0-9a-fA-F]") string(REPEAT "${hex_digit}" 5 hex_5) set(hex_digit_maybe "${hex_digit}?") string(REPEAT "${hex_digit_maybe}" 35 hex_35) # A sha1 would have at least 5 hex digits followed by 35 optional hex digits. set(sha1_regex "^${hex_5}${hex_35}$") if("${value}" MATCHES "${sha1_regex}") set(result TRUE) else() set(result FALSE) endif() set(${out_var} "${result}" PARENT_SCOPE) endfunction() # Directly updates the submodule sha in the supermodule, without having to checkout the submodule # first. # This is useful to be able to clone a specific revision with --depth 1 when using the "sync to # module" feature. # Causes the super module to have a "staged" change for the given submodule. # Can only be used with a sha1, not a branch or tag or other refspec, because there might not be # any repo info yet to resolve that refspec. function(qt_internal_tl_modify_submodule_sha module revision working_directory) qt_internal_tl_handle_verbose_git_operations() # This mode means 'treat path as a git submodule'. set(mode "160000") execute_process( COMMAND git update-index --add --cacheinfo "${mode},${revision},${module}" RESULT_VARIABLE git_result WORKING_DIRECTORY "${working_directory}" ${swallow_output} ) if(git_result) message(FATAL_ERROR "Failed to set initial submodule revision for '${module}'") endif() endfunction() # Unstages a previously staged change to the submodule sha in the supermodule. # This should be run after qt_internal_tl_modify_submodule_sha and the submodule update operation, # to not accidentally stage the change to the supermodule. # It might still lieave the worktree dirty, because the checked out revision might be different # from the one the supermodule expects, but that's fine, that's the point of the sync-to script. function(qt_internal_tl_unstage_submodule_sha module working_directory) qt_internal_tl_handle_verbose_git_operations() execute_process( COMMAND git restore --staged "${module}" RESULT_VARIABLE git_result WORKING_DIRECTORY "${working_directory}" ${swallow_output} ) if(git_result) message(FATAL_ERROR "Failed to unstage submodule revision change for '${module}'") endif() endfunction() # Transforms a refspec into a commit sha1. # Useful for git commands that can't take a refspec. function(qt_internal_tl_get_refspec_as_sha) set(opt_args FATAL ) set(single_args REF_SPEC WORKING_DIRECTORY OUT_VAR ) set(multi_args "") cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") qt_internal_tl_validate_all_args_are_parsed(arg) if(NOT arg_REF_SPEC) message(FATAL_ERROR "REF_SPEC is required") endif() if(NOT arg_WORKING_DIRECTORY) message(FATAL_ERROR "WORKING_DIRECTORY is required") endif() if(NOT arg_OUT_VAR) message(FATAL_ERROR "OUT_VAR is required") endif() execute_process( COMMAND "git" "rev-parse" "${arg_REF_SPEC}" WORKING_DIRECTORY "${arg_WORKING_DIRECTORY}" RESULT_VARIABLE git_result OUTPUT_VARIABLE git_stdout ERROR_VARIABLE git_stderr OUTPUT_STRIP_TRAILING_WHITESPACE ) if(git_result) message(WARNING "${git_stdout}") if(arg_FATAL) set(message_type FATAL_ERROR) else() set(message_type WARNING) endif() message("${message_type}" "Failed to get sha1 of ${arg_REF_SPEC} in '${arg_WORKING_DIRECTORY}': ${git_stderr}") endif() string(STRIP "${git_stdout}" git_stdout) set(${arg_OUT_VAR} "${git_stdout}" PARENT_SCOPE) endfunction() # Runs `submodule update --init` for a submodule. # # If REF_SPEC is passed and is a valid commit sha1, sets the current active submodule sha1 in the # super module to the given sha1. This is useful for cloning a specific sha1 with --depth 1. # # GIT_DEPTH passes the given --depth for the submodule update operation. # # SHOW_PROGRESS passes --progress to the submodule update operation. # # OUT_VAR_RESULT - set to TRUE or FALSE depending on whether the submodule update init succeeded. function(qt_internal_tl_run_submodule_update_init module) set(opt_args FAILURE_IS_WARNING SHOW_PROGRESS ) set(single_args REF_SPEC GIT_DEPTH FAILURE_MESSAGE WORKING_DIRECTORY OUT_VAR_RESULT ) set(multi_args "") cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") qt_internal_tl_validate_all_args_are_parsed(arg) qt_internal_tl_handle_verbose_git_operations() if(NOT arg_WORKING_DIRECTORY) message(FATAL_ERROR "WORKING_DIRECTORY is required") endif() qt_internal_tl_is_sha1_ish("${arg_REF_SPEC}" is_sha1_revision) # We can only modify the submodule sha1 if we are given a sha1 reference, not any kind of # refspec. if(arg_REF_SPEC AND is_sha1_revision) qt_internal_tl_modify_submodule_sha("${module}" "${arg_REF_SPEC}" "${arg_WORKING_DIRECTORY}") endif() set(args "") set(extra_flags "$ENV{QT_TL_SUBMODULE_UPDATE_FLAGS}") if(extra_flags) list(APPEND args ${extra_flags}) endif() if(arg_GIT_DEPTH) list(APPEND args --depth "${arg_GIT_DEPTH}") endif() if(arg_SHOW_PROGRESS) list(APPEND args --progress) endif() execute_process( COMMAND "git" "submodule" "update" "--init" ${args} "${module}" RESULT_VARIABLE git_result WORKING_DIRECTORY "${arg_WORKING_DIRECTORY}" ${swallow_output} ) if(arg_REF_SPEC AND is_sha1_revision) qt_internal_tl_unstage_submodule_sha("${module}" "${arg_WORKING_DIRECTORY}") endif() if(git_result) set(result FALSE) if(arg_FAILURE_IS_WARNING) set(message_type WARNING) else() set(message_type FATAL_ERROR) endif() message(${message_type} "${arg_FAILURE_MESSAGE}") else() set(result TRUE) endif() if(arg_OUT_VAR_RESULT) set("${arg_OUT_VAR_RESULT}" "${result}" PARENT_SCOPE) endif() endfunction() # Clones a 'qt/${REPO_NAME}' repo from code.qt.io. function(qt_internal_tl_git_clone_repo) set(opt_args SHOW_PROGRESS ) set(single_args REPO_NAME GIT_DEPTH REMOTE_URL_BASE WORKING_DIRECTORY ) set(multi_args "") cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") qt_internal_tl_validate_all_args_are_parsed(arg) qt_internal_tl_handle_verbose_git_operations() if(NOT arg_WORKING_DIRECTORY) message(FATAL_ERROR "WORKING_DIRECTORY is required") endif() if(NOT arg_REPO_NAME) message(FATAL_ERROR "REPO_NAME is required") endif() if(arg_REMOTE_URL_BASE) set(remote_url_base "${arg_REMOTE_URL_BASE}") else() set(remote_url_base "https://code.qt.io/qt/") endif() set(remote_url "${remote_url_base}${arg_REPO_NAME}.git") message(NOTICE "Cloning '${arg_REPO_NAME}' from '${remote_url}'") set(clone_args "") if(arg_GIT_DEPTH) list(APPEND clone_args --depth "${arg_GIT_DEPTH}") endif() if(arg_SHOW_PROGRESS) list(APPEND clone_args --progress) endif() # Note that cloning does not allow fetching a specific sha1 directly if --depth is # specified. It can only take a branch or tag name. So we don't pass REF_SPEC here. execute_process( COMMAND "git" "clone" "${remote_url}" ${clone_args} WORKING_DIRECTORY "${arg_WORKING_DIRECTORY}" RESULT_VARIABLE git_result ${swallow_output} ) if(git_result) message(FATAL_ERROR "Failed to clone '${module}' from '${remote_url}': ${git_output}") endif() endfunction() # Checks out a submodule to a given refspec, and runs 'git submodule update' in the submodule # directory. # If a regular checkout does not work, a detached checkout is attempted. function(qt_internal_checkout module revision) set(opt_args SHOW_PROGRESS ) set(single_args REMOTE_NAME WORKING_DIRECTORY ) set(multi_args "") cmake_parse_arguments(PARSE_ARGV 2 arg "${opt_args}" "${single_args}" "${multi_args}") qt_internal_tl_validate_all_args_are_parsed(arg) qt_internal_tl_handle_verbose_git_operations() if(NOT arg_WORKING_DIRECTORY) message(FATAL_ERROR "WORKING_DIRECTORY is required") endif() if(NOT arg_REMOTE_NAME) message(FATAL_ERROR "REMOTE_NAME is required") endif() set(shallow_args "") if(arg_SHOW_PROGRESS) list(APPEND shallow_args SHOW_PROGRESS) endif() qt_internal_tl_handle_shallow_repo( REF_SPEC "${revision}" REMOTE_NAME "${arg_REMOTE_NAME}" WORKING_DIRECTORY "${arg_WORKING_DIRECTORY}/${module}" ${shallow_args} ) message(NOTICE "Checking '${module}' out to revision '${revision}'") execute_process( COMMAND "git" "checkout" "${revision}" WORKING_DIRECTORY "${arg_WORKING_DIRECTORY}/${module}" RESULT_VARIABLE git_result ${swallow_output} ) if (git_result EQUAL 128) message(WARNING "${git_output}, trying detached checkout") execute_process( COMMAND "git" "checkout" "--detach" "${revision}" WORKING_DIRECTORY "${arg_WORKING_DIRECTORY}/${module}" RESULT_VARIABLE git_result ${swallow_output} ) endif() if (git_result) message(FATAL_ERROR "Failed to check '${module}' out to '${revision}': ${git_output}") endif() set(args "") set(extra_flags "$ENV{QT_TL_SUBMODULE_UPDATE_FLAGS}") if(extra_flags) list(APPEND args ${extra_flags}) endif() if(arg_SHOW_PROGRESS) list(APPEND args --progress) endif() execute_process( COMMAND "git" "submodule" "update" ${args} WORKING_DIRECTORY "${arg_WORKING_DIRECTORY}/${module}" RESULT_VARIABLE git_result ${swallow_output} ) endfunction() # Clones or creates a worktree, or initializes a submodule for $dependency, using the source of # $dependent. # Example dependent: qtdeclarative # Example dependency: qtbase function(qt_internal_get_dependency dependent dependency) set(opt_args SHOW_PROGRESS ) set(single_args GIT_DEPTH REMOTE_NAME REF_SPEC ) set(multi_args "") cmake_parse_arguments(PARSE_ARGV 2 arg "${opt_args}" "${single_args}" "${multi_args}") qt_internal_tl_validate_all_args_are_parsed(arg) qt_internal_tl_handle_verbose_git_operations() if(NOT arg_REMOTE_NAME) message(FATAL_ERROR "REMOTE_NAME is required") endif() set(show_progress_args "") if(arg_SHOW_PROGRESS) set(show_progress_args SHOW_PROGRESS) endif() # This will hold the path to parent dir of the main ${dependent} worktree, regardless if it's a # clone or a git worktree. # So if dependent is 'src/qt6/qtshadertools' # gitdir will be 'src/qt6' # If dependent is 'worktrees/6.8-worktree/qtshadertools' # gitdir will still be 'src/qt6', not 'worktrees/6.8-worktree' set(gitdir "") # The remote url. set(remote "") # Worktree of dependent, aka who depends on dependency. set(dependent_path "${CMAKE_CURRENT_SOURCE_DIR}/${dependent}") # Try to get the dependent worktree git dir. execute_process( COMMAND "git" "rev-parse" "--absolute-git-dir" WORKING_DIRECTORY "${dependent_path}" RESULT_VARIABLE git_result OUTPUT_VARIABLE git_stdout ERROR_VARIABLE git_stderr OUTPUT_STRIP_TRAILING_WHITESPACE ) message(DEBUG "Original gitdir for '${dependent_path}' is '${git_stdout}'") string(FIND "${git_stdout}" "${module}" index) string(SUBSTRING "${git_stdout}" 0 ${index} gitdir) string(FIND "${gitdir}" ".git/modules" index) if(index GREATER -1) # submodules have not been absorbed string(SUBSTRING "${gitdir}" 0 ${index} gitdir) endif() message(DEBUG "Will check computed '${gitdir}' for worktrees and clones.") execute_process( COMMAND "git" "remote" "get-url" "${arg_REMOTE_NAME}" WORKING_DIRECTORY "${dependent_path}" RESULT_VARIABLE git_result OUTPUT_VARIABLE git_stdout ERROR_VARIABLE git_stderr OUTPUT_STRIP_TRAILING_WHITESPACE ) string(FIND "${git_stdout}" "${dependent}.git" index) string(SUBSTRING "${git_stdout}" 0 ${index} remote) message(DEBUG "Original remote for '${dependent_path}' is '${git_stdout}'") set(maybe_super_module_path "${gitdir}") set(maybe_submodule_path "${maybe_super_module_path}${dependency}") set(maybe_submodule_git_path "${maybe_submodule_path}/.git") set(maybe_existing_worktree_path "${maybe_submodule_path}") if(EXISTS "${maybe_super_module_path}.gitmodules" AND NOT EXISTS "${maybe_submodule_git_path}") set(use_submodule_init TRUE) message(DEBUG "Will attempt to initialize submodule using supermodule ${maybe_super_module_path}") else() set(use_submodule_init FALSE) if(EXISTS "${maybe_existing_worktree_path}") message(DEBUG "Will attempt to use worktree from ${maybe_existing_worktree_path}") else() message(DEBUG "Will clone from ${remote}") endif() endif() if(use_submodule_init) # super repo exists, but the submodule we need does not - try to initialize message(NOTICE "Initializing submodule '${dependency}' from ${maybe_super_module_path}") set(args FAILURE_MESSAGE "Failed to initialize submodule '${dependency}' from ${maybe_super_module_path}" # Ignore errors, fall back to an independent clone below. FAILURE_IS_WARNING OUT_VAR_RESULT submodule_update_init_result WORKING_DIRECTORY "${gitdir}" ${show_progress_args} ) if(arg_GIT_DEPTH) list(APPEND args GIT_DEPTH "${arg_GIT_DEPTH}" REF_SPEC "${arg_REF_SPEC}" ) endif() qt_internal_tl_run_submodule_update_init("${dependency}" ${args}) endif() # If the submodule was initialized in the super repo in the code above, and the location where # we're supposed to clone the dependency is the same, skip trying to clone the dependency or # setting up a worktree, because it's already there. set(new_dependency_path "${CMAKE_CURRENT_SOURCE_DIR}/${dependency}") if(EXISTS "${new_dependency_path}" AND "${new_dependency_path}" STREQUAL "${maybe_existing_worktree_path}") return() endif() if(EXISTS "${maybe_existing_worktree_path}") # for the module we want, there seems to be a clone parallel to what we have message(NOTICE "Adding worktree for ${dependency} from ${gitdir}${dependency}") execute_process( COMMAND "git" "worktree" "add" "--detach" "${new_dependency_path}" WORKING_DIRECTORY "${maybe_existing_worktree_path}" RESULT_VARIABLE git_result ${swallow_output} ) if(git_result) message(FATAL_ERROR "Failed to add worktree '${module}' from '${new_dependency_path}': ${git_output}") endif() else() # We didn't find an existing clone or worktree, so clone from the same remote. set(clone_args "") if(arg_GIT_DEPTH) list(APPEND clone_args GIT_DEPTH "${arg_GIT_DEPTH}") endif() if(arg_SHOW_PROGRESS) list(APPEND clone_args SHOW_PROGRESS) endif() qt_internal_tl_git_clone_repo( REPO_NAME "${dependency}" REMOTE_URL_BASE "${remote}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" ${clone_args} ) endif() endfunction() # Syncs a submodule to a given refspec, collects its dependencies, and then checks out # the submodule and its dependencies to a consistent set, according to the submodule # dependencies.yaml file. # # A special case is when the module is ".", in which case all submodules are checked out to the # given refspec, e.g. check out everything to origin/dev/HEAD. # # Initializes the submodule and any of its dependencies if they are not already initialized, when # executed in a qt5.git checkout. # # Clones the specified submodule from code.qt.io if it missing, and not in a qt5.git checkout. function(qt_internal_sync_to module) set(opt_args VERBOSE SHOW_PROGRESS ) set(single_args SYNC_REF REMOTE_NAME GIT_DEPTH ) set(multi_args "") cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") qt_internal_tl_validate_all_args_are_parsed(arg) if(arg_VERBOSE) # This is meant to trickle into scopes of other functions as well. set(VERBOSE TRUE) endif() set(show_progress_args "") if(arg_SHOW_PROGRESS) set(show_progress_args SHOW_PROGRESS) endif() if(arg_REMOTE_NAME) set(remote_name "${arg_REMOTE_NAME}") else() set(remote_name "origin") endif() set(revision "${arg_SYNC_REF}") # Special casing "." as the target module - checkout all initialized submodules to $revision. # If revision is unset, check out to dev. if("${module}" STREQUAL ".") if(NOT revision) set(revision "dev") endif() qt_internal_find_modules(modules) foreach(module IN LISTS modules) qt_internal_checkout("${module}" "${revision}" ${show_progress_args} REMOTE_NAME "${remote_name}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" ) endforeach() return() endif() # If no revision given, checkout to the HEAD as specified by the super module. if(NOT revision) set(revision "HEAD") endif() set(submodule_path "${CMAKE_CURRENT_SOURCE_DIR}/${module}") set(submodule_git_path "${submodule_path}/.git") # We are in a qt5.git dir, but the requested submodule is not initialized yet, try to # initialize it. qt_internal_tl_is_super_repo(is_super_repo) if(is_super_repo AND NOT EXISTS "${submodule_git_path}") message(NOTICE "Initializing submodule '${module}' within supermodule.") set(args FAILURE_MESSAGE "Failed to initialize initial submodule '${module}'" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" ${show_progress_args} ) if(arg_GIT_DEPTH) list(APPEND args GIT_DEPTH "${arg_GIT_DEPTH}" REF_SPEC "${revision}" ) endif() qt_internal_tl_run_submodule_update_init("${module}" ${args}) endif() # If we were in a qt5.git dir, the submodule should have been initialized by now. # If we were in some random src/ dir, we need to manually clone the repo. if(NOT EXISTS "${submodule_path}") qt_internal_tl_git_clone_repo( REPO_NAME "${module}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" ${show_progress_args} ) endif() if(NOT EXISTS "${submodule_git_path}") message(FATAL_ERROR "No worktree for '${module}' found in '${submodule_path}'") endif() # Check out the submodule to the given refspec. qt_internal_checkout("${module}" "${revision}" ${show_progress_args} REMOTE_NAME "${remote_name}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" ) qt_internal_resolve_module_dependencies(${module} initial_dependencies initial_revisions) if(initial_dependencies) foreach(dependency ${initial_dependencies}) if(dependency MATCHES "^tqtc-") message(WARNING "Handling of tqtc- repos will likely fail. Fixing this is non-trivial.") break() endif() endforeach() endif() set(revision "") set(should_visit_dependencies "1") # Load all dependencies for $module, then iterate over the dependencies in reverse order, # and check out the first that isn't already at the required revision. # Repeat everything (we need to reload dependencies after each checkout) until no more checkouts # are done. while(${should_visit_dependencies}) qt_internal_resolve_module_dependencies(${module} dependencies revisions) message(DEBUG "${module} dependencies: ${dependencies}") message(DEBUG "${module} revisions : ${revisions}") list(LENGTH dependencies count) if (count EQUAL "0") message(NOTICE "Module ${module} has no dependencies") return() endif() math(EXPR count "${count} - 1") set(should_visit_dependencies 0) foreach(i RANGE ${count} 0 -1 ) list(GET dependencies ${i} dependency) list(GET revisions ${i} revision) if ("${revision}" STREQUAL "HEAD") message(DEBUG "Not changing checked out revision of ${dependency}") continue() endif() # When in a super module, the dependency directory might exist, but is empty if the # submodule was not yet initiallized. Check its existence and initialization state # by looking at the existence of the .git file or directory. if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${dependency}/.git") message(DEBUG "No worktree for '${dependency}' found in '${CMAKE_CURRENT_SOURCE_DIR}'. " "Trying to acquire it." ) set(args ${show_progress_args}) if(arg_GIT_DEPTH) list(APPEND args GIT_DEPTH "${arg_GIT_DEPTH}") endif() qt_internal_get_dependency("${module}" "${dependency}" REF_SPEC "${revision}" REMOTE_NAME "${remote_name}" ${args} ) set(should_visit_dependencies 1) endif() qt_internal_tl_get_refspec_as_sha( REF_SPEC "HEAD" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${dependency}" OUT_VAR head_ref ) if("${head_ref}" STREQUAL "${revision}") message(DEBUG "The dependency ${dependency} is already checked out to ${revision}. " "Continuing to next dependency." ) continue() endif() qt_internal_checkout("${dependency}" "${revision}" ${show_progress_args} REMOTE_NAME "${remote_name}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" ) set(should_visit_dependencies 1) # Start revisiting the dependencies in the while loop. break() endforeach() endwhile() message(DEBUG "Module syncing finished.") endfunction() # Runs user specified command for all qt repositories in qt directory. # Similar to git submodule foreach, except without relying on .gitmodules existing. # Useful for worktree checkouts. function(qt_internal_foreach_repo_run) cmake_parse_arguments(PARSE_ARGV 0 arg "" "" "ARGS" ) if(NOT arg_ARGS) message(FATAL_ERROR "No arguments specified to qt_internal_foreach_repo_run") endif() separate_arguments(args NATIVE_COMMAND "${arg_ARGS}") # Find the qt repos qt_internal_find_modules(modules) # Hack to support color output on unix systems # https://stackoverflow.com/questions/18968979/how-to-make-colorized-message-with-cmake execute_process(COMMAND /usr/bin/tty OUTPUT_VARIABLE tty_name RESULT_VARIABLE tty_exit_code OUTPUT_STRIP_TRAILING_WHITESPACE ) set(color_supported FALSE) set(output_goes_where "") if(NOT tty_exit_CODE AND tty_name) set(color_supported TRUE) set(output_goes_where "OUTPUT_FILE" "${tty_name}") endif() # Count successes and failures. set(count_success "0") set(count_failure "0") # Show colored error markers. set(color "--normal") if(color_supported) set(color "--red") endif() foreach(module IN LISTS modules) message("Entering '${module}'") execute_process( COMMAND ${args} WORKING_DIRECTORY "${module}" ${output_goes_where} RESULT_VARIABLE cmd_result ) if(cmd_result) math(EXPR count_failure "${count_failure}+1") # cmake_echo_color is undocumented, but lets us output colors and control newlines. execute_process( COMMAND ${CMAKE_COMMAND} -E env CLICOLOR_FORCE=1 ${CMAKE_COMMAND} -E cmake_echo_color "${color}" "Process execution failed here ^^^^^^^^^^^^^^^^^^^^" ) else() math(EXPR count_success "${count_success}+1") endif() endforeach() # Show summary with colors. set(color "--normal") if(count_failure AND color_supported) set(color "--red") endif() message("\nSummary\n=======\n") execute_process( COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --normal --no-newline "Failures: " ) execute_process( COMMAND ${CMAKE_COMMAND} -E env CLICOLOR_FORCE=1 ${CMAKE_COMMAND} -E cmake_echo_color "${color}" "${count_failure}" ) message("Successes: ${count_success}") endfunction() # The function collects repos and dependencies that are required to build # repos listed in ARGN. If the BUILD_ is defined the 'repo' will be # excluded from the list. function(qt_internal_collect_modules_only out_repos) set(initial_modules "${ARGN}") get_filename_component(qt5_repo_dir "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE) # Overriding CMAKE_CURRENT_SOURCE_DIR is ugly but works set(CMAKE_CURRENT_SOURCE_DIR "${qt5_repo_dir}") if(NOT initial_modules) qt_internal_find_modules(initial_modules) endif() qt_internal_sort_module_dependencies("${initial_modules}" ${out_repos}) foreach(module IN LISTS ${out_repos}) # Check for unmet dependencies if(DEFINED BUILD_${module} AND NOT BUILD_${module}) list(REMOVE_ITEM ${out_repos} ${module}) continue() endif() get_property(required_deps GLOBAL PROPERTY QT_REQUIRED_DEPS_FOR_${module}) get_property(dependencies GLOBAL PROPERTY QT_DEPS_FOR_${module}) foreach(dep IN LISTS dependencies) set(required FALSE) if(dep IN_LIST required_deps) set(required TRUE) endif() if(required AND DEFINED BUILD_${dep} AND NOT BUILD_${dep}) set(BUILD_${module} FALSE) list(REMOVE_ITEM ${out_repos} ${module}) break() endif() endforeach() endforeach() set(${out_repos} "${${out_repos}}" PARENT_SCOPE) endfunction()