diff options
Diffstat (limited to 'cmake/QtIRGitHelpers.cmake')
-rw-r--r-- | cmake/QtIRGitHelpers.cmake | 1151 |
1 files changed, 1151 insertions, 0 deletions
diff --git a/cmake/QtIRGitHelpers.cmake b/cmake/QtIRGitHelpers.cmake new file mode 100644 index 00000000..288f4ac7 --- /dev/null +++ b/cmake/QtIRGitHelpers.cmake @@ -0,0 +1,1151 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Returns the git version. +function(qt_ir_get_git_version out_var) + qt_ir_get_option_value(perl-identical-output perl_identical_output_for_tests) + + set(extra_args "") + if(perl_identical_output_for_tests) + set(extra_args FORCE_QUIET) + endif() + + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git version ${extra_args} + OUT_OUTPUT_VAR git_output + ERROR_MESSAGE "Failed to retrieve git version") + + string(REGEX REPLACE "^git version ([0-9]+)\\.([0-9]+)\\.([0-9]+).*$" "\\1.\\2.\\3" + version "${git_output}") + if(NOT version) + message(FATAL_ERROR "Failed to parse git version: ${git_output}, expected [d]+.[d]+.[d]+") + endif() + + set(${out_var} "${version}" PARENT_SCOPE) +endfunction() + +# Returns the git version, but caches the result in a global property. +function(qt_ir_get_git_version_cached out_var) + get_property(version GLOBAL PROPERTY _qt_git_version) + if(NOT version) + qt_ir_get_git_version(version) + endif() + + set_property(GLOBAL PROPERTY _qt_git_version "${version}") + + set(${out_var} "${version}" PARENT_SCOPE) +endfunction() + +# Returns whether git supports the git submodule --progress option. +function(qt_ir_is_git_progress_supported out_var) + qt_ir_get_git_version_cached(version) + if(version VERSION_GREATER_EQUAL "2.11") + set(${out_var} TRUE PARENT_SCOPE) + else() + set(${out_var} FALSE PARENT_SCOPE) + endif() +endfunction() + +# Get the mirror with trailing slashes removed. +function(qt_ir_get_mirror out_var) + qt_ir_get_option_value(mirror mirror) + qt_ir_get_option_value(berlin berlin) + qt_ir_get_option_value(oslo oslo) + + if(berlin) + set(mirror "git://hegel/qt/") + elseif(oslo) + set(mirror "git://qilin/qt/") + endif() + + # Replace any double trailing slashes from end of mirror + string(REGEX REPLACE "//+$" "/" mirror "${mirror}") + + set(${out_var} "${mirror}" PARENT_SCOPE) +endfunction() + +# Sets up the commit template for a submodule. +function(qt_ir_setup_commit_template commit_template_dir working_directory) + set(template "${commit_template_dir}/.commit-template") + if(NOT EXISTS "${template}") + return() + endif() + + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git config commit.template "${template}" + ERROR_MESSAGE "Failed to setup commit template" + WORKING_DIRECTORY "${working_directory}") +endfunction() + +# Initializes a list of submodules. This does not them, but just +# sets up the .git/config file submodule.$submodule_name.url based on the .gitmodules template file. +function(qt_ir_run_git_submodule_init submodules working_directory) + set(submodule_dirs "") + foreach(submodule_name IN LISTS submodules) + set(submodule_path "${${prefix}_${submodule_name}_path}") + list(APPEND submodule_dirs "${submodule_name}") + endforeach() + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git submodule init ${submodule_dirs} + ERROR_MESSAGE "Failed to git submodule init ${submodule_dirs}" + WORKING_DIRECTORY "${working_directory}") + + qt_ir_setup_commit_template("${working_directory}" "${working_directory}") +endfunction() + +# Add gerrit remotes to the repository. +function(qt_ir_add_git_remotes repo_name working_directory) + set(gerrit_ssh_base "ssh://@USER@codereview.qt-project.org@PORT@/qt/") + set(gerrit_repo_url "${gerrit_ssh_base}") + + qt_ir_get_option_value(codereview-username username) + + # If given a username, make a "verbose" remote. + # Otherwise, rely on proper SSH configuration. + if(username) + string(REPLACE "@USER@" "${username}@" gerrit_repo_url "${gerrit_repo_url}") + string(REPLACE "@PORT@" ":29418" gerrit_repo_url "${gerrit_repo_url}") + else() + string(REPLACE "@USER@" "" gerrit_repo_url "${gerrit_repo_url}") + string(REPLACE "@PORT@" "" gerrit_repo_url "${gerrit_repo_url}") + endif() + + string(APPEND gerrit_repo_url "${repo_name}") + + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git config remote.gerrit.url "${gerrit_repo_url}" + ERROR_MESSAGE "Failed to set gerrit repo url" + WORKING_DIRECTORY "${working_directory}") + + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS + git config remote.gerrit.fetch "+refs/heads/*:refs/remotes/gerrit/*" "/heads/" + ERROR_MESSAGE "Failed to set gerrit repo fetch refspec" + WORKING_DIRECTORY "${working_directory}") +endfunction() + +# Handles the copy-objects option, which is used to detach alternates. +# A copy of all git objects are made from the alternate repository to the current repository. +# Then the alternates reference is removed. +function(qt_ir_handle_detach_alternates working_directory) + qt_ir_get_option_value(copy-objects should_detach) + if(NOT should_detach) + return() + endif() + + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git repack -a + ERROR_MESSAGE "Failed to repack objects to detach alternates" + WORKING_DIRECTORY "${working_directory}") + + set(alternates_path "${working_directory}/.git/objects/info/alternates") + if(EXISTS "${alternates_path}") + file(REMOVE "${alternates_path}") + if(EXISTS "${alternates_path}") + message(FATAL_ERROR "Failed to remove alternates file: ${alternates_path}") + endif() + endif() +endfunction() + +# Clones a submodule, unless it was previously cloned. +# When cloning, checks out a specific branch if requested, otherwise does not +# checkout any files yet, mimicking a bare repo. +# Sets up an alternates link if requested. +# Detaches alternates if requested. +# Fetches refs if requested. +# Adds a gerrit git remote. +# Sets up the commit template for the submodule. +function(qt_ir_clone_one_submodule submodule_name) + set(options + CHECKOUT_BRANCH + FETCH + ) + set(oneValueArgs + ALTERNATES + BASE_URL + WORKING_DIRECTORY + ) + set(multiValueArgs "") + cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + qt_ir_get_working_directory_from_arg(working_directory) + + set(clone_args "") + set(submodule_path "${${prefix}_${submodule_name}_path}") + + if(arg_ALTERNATES) + # alternates is a qt5 repo, so the submodule will be under that. + set(alternates_dir "${arg_ALTERNATES}/${submodule_path}/.git") + if(EXISTS "${alternates_dir}") + list(APPEND clone_args --reference "${arg_ALTERNATES}/${submodule_path}") + else() + message(WARNING "'${arg_ALTERNATES}/${submodule_path}' not found, " + "ignoring alternate for this submodule") + endif() + endif() + + if(NOT EXISTS "${working_directory}/${submodule_path}/.git") + set(should_clone TRUE) + else() + set(should_clone FALSE) + endif() + + set(submodule_base_git_path "${${prefix}_${submodule_name}_base_git_path}") + + set(submodule_url "${submodule_base_git_path}") + qt_ir_has_url_scheme("${submodule_url}" has_url_scheme) + if(NOT has_url_scheme AND arg_BASE_URL) + set(submodule_url "${arg_BASE_URL}${submodule_url}") + endif() + + qt_ir_get_mirror(mirror_url) + set(mirror "") + if(NOT has_url_scheme AND mirror_url AND (should_clone OR arg_FETCH)) + set(mirror "${mirror_url}${submodule_base_git_path}") + endif() + + set(mirror_or_original_url "${submodule_url}") + if(mirror) + # Only use the mirror if it can be reached. + # Access a non-existing ref so no output is shown. It should still + # succeed if the mirror is accessible. + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git ls-remote "${mirror}" "test/if/mirror/exists" + WORKING_DIRECTORY "${working_directory}" + NO_HANDLE_ERROR + OUT_RESULT_VAR proc_result) + if(NOT proc_result EQUAL 0) + message("mirror [${mirror}] is not accessible; ${submodule_url} will be used") + set(mirror "") + else() + set(mirror_or_original_url "${mirror}") + endif() + endif() + + set(submodule_branch "${${prefix}_${submodule_name}_branch}") + + qt_ir_is_git_progress_supported(is_git_progress_supported) + qt_ir_get_option_value(quiet quiet) + qt_ir_get_option_value(perl-identical-output perl_identical_output_for_tests) + + set(progress_args "") + if(is_git_progress_supported AND NOT quiet AND NOT perl_identical_output_for_tests) + set(progress_args --progress) + endif() + + if(should_clone) + if(arg_CHECKOUT_BRANCH) + list(APPEND clone_args --branch "${submodule_branch}") + else() + list(APPEND clone_args --no-checkout) + endif() + list(APPEND clone_args ${progress_args}) + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git clone ${clone_args} "${mirror_or_original_url}" "${submodule_path}" + ERROR_MESSAGE "Failed to clone submodule '${submodule_name}'" + WORKING_DIRECTORY "${working_directory}") + endif() + + set(submodule_working_dir "${working_directory}/${submodule_path}") + + if(mirror) + # This is only for the user's convenience - we make no use of it. + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git config "remote.mirror.url" "${mirror}" + ERROR_MESSAGE "Failed to set git config remote.mirror.url to ${mirror}" + WORKING_DIRECTORY "${submodule_working_dir}") + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git config "remote.mirror.fetch" "+refs/heads/*:refs/remotes/mirror/*" + ERROR_MESSAGE "Failed to set git config remote.mirror.fetch" + WORKING_DIRECTORY "${submodule_working_dir}") + endif() + + if(NOT should_clone AND arg_FETCH) + # If we didn't clone, fetch from the right location. We always update + # the origin remote, so that submodule update --remote works. + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git config remote.origin.url "${mirror_or_original_url}" + ERROR_MESSAGE "Failed to set remote origin url" + WORKING_DIRECTORY "${submodule_working_dir}") + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git fetch origin ${progress_args} + ERROR_MESSAGE "Failed to fetch origin" + WORKING_DIRECTORY "${submodule_working_dir}") + endif() + + if(NOT (should_clone OR arg_FETCH) OR mirror) + # Leave the origin configured to the canonical URL. It's already correct + # if we cloned/fetched without a mirror; otherwise it may be anything. + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git config remote.origin.url "${submodule_url}" + ERROR_MESSAGE "Failed to set remote origin url" + WORKING_DIRECTORY "${submodule_working_dir}") +endif() + + set(commit_template_dir "${working_directory}") + qt_ir_setup_commit_template("${commit_template_dir}" "${submodule_working_dir}") + + if(NOT has_url_scheme) + qt_ir_add_git_remotes("${submodule_base_git_path}" "${submodule_working_dir}") + endif() + + qt_ir_handle_detach_alternates("${submodule_working_dir}") +endfunction() + +# Get list of submodules that were previously initialized, by looking at the .git/config file. +function(qt_ir_get_already_initialized_submodules prefix + out_var_already_initialized_submodules + parent_repo_base_git_path + working_directory + ) + + qt_ir_parse_git_config_file_contents("${prefix}" + READ_GIT_CONFIG + PARENT_REPO_BASE_GIT_PATH "${parent_repo_base_git_path}" + WORKING_DIRECTORY "${working_directory}" + ) + + set(${out_var_already_initialized_submodules} "${${prefix}_submodules}" PARENT_SCOPE) +endfunction() + +# If init-repository --force is called with a different subset, remove +# previously initialized submodules from the .git/config file. +# Also mark submodules as ignored if requested. +function(qt_ir_handle_submodule_removal_and_ignoring prefix + included_submodules + parent_repo_base_git_path + working_directory + ) + + qt_ir_get_option_value(ignore-submodules ignore_submodules) + + qt_ir_get_already_initialized_submodules("${prefix}" + already_initialized_submodules + "${parent_repo_base_git_path}" + "${working_directory}") + + foreach(submodule_name IN LISTS already_initialized_submodules) + if(NOT submodule_name IN_LIST included_submodules) + # If a submodule is not included in the list of submodules to be initialized, + # and it was previously initialized, then remove it from the config. + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git config --remove-section "submodule.${submodule_name}" + ERROR_MESSAGE "Failed to deinit submodule '${submodule_name}'" + WORKING_DIRECTORY "${working_directory}") + continue() + endif() + if(ignore_submodules) + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git config "submodule.${submodule_name}.ignore" all + ERROR_MESSAGE "Failed to ignore submodule '${submodule_name}'" + WORKING_DIRECTORY "${working_directory}") + endif() + endforeach() +endfunction() + +# Checks if the submodule is dirty (has uncommited changes). +function(qt_ir_check_if_dirty_submodule submodule_name working_directory out_is_dirty) + set(submodule_path "${working_directory}/${${prefix}_${submodule_name}_path}") + if(NOT EXISTS "${submodule_path}/.git") + return() + endif() + + qt_ir_execute_process_and_log_and_handle_error( + FORCE_QUIET + COMMAND_ARGS git status --porcelain --untracked=no --ignore-submodules=all + WORKING_DIRECTORY "${submodule_path}" + ERROR_MESSAGE "Failed to get dirty status for '${submodule_name}'" + OUT_OUTPUT_VAR git_output) + + string(STRIP "${git_output}" git_output) + string(REPLACE "\n" ";" git_lines "${git_output}") + + # After a git clone --no-checkout, git status reports all files as + # staged for deletion, but we still want to update the submodule. + # It's unlikely that a genuinely dirty index would have _only_ this + # type of modifications, and it doesn't seem like a horribly big deal + # to lose them anyway, so ignore them. + # @sts = grep(!/^D /, @sts); + # Filter list that starts with the regex + list(FILTER git_lines EXCLUDE REGEX "^D ") + + if(git_lines) + message(STATUS "${submodule_name} is dirty.") + set(is_dirty TRUE) + else() + set(is_dirty FALSE) + endif() + + set(${out_is_dirty} "${is_dirty}" PARENT_SCOPE) +endfunction() + +# Checks if any submodules are dirty and exits early if any are. +function(qt_ir_handle_dirty_submodule submodules working_directory) + set(any_is_dirty FALSE) + foreach(submodule_name IN LISTS submodules) + qt_ir_check_if_dirty_submodule("${submodule_name}" "${working_directory}" is_dirty) + if(is_dirty) + set(any_is_dirty TRUE) + endif() + endforeach() + + if(any_is_dirty) + message(FATAL_ERROR "Dirty submodule(s) present; cannot proceed.") + endif() +endfunction() + +# If the branch option is set, checkout the branch specified in the .gitmodules file. +function(qt_ir_handle_branch_option prefix submodule_name working_directory) + set(branch_name "${${prefix}_${submodule_name}_branch}") + if(NOT branch_name) + message(FATAL_ERROR "No branch defined for submodule '${submodule_name}'") + endif() + + set(repo_dir "${working_directory}/${${prefix}_${submodule_name}_path}") + qt_ir_execute_process_and_log_and_handle_error( + FORCE_QUIET + COMMAND_ARGS git rev-parse -q --verify ${branch_name} + WORKING_DIRECTORY "${repo_dir}" + NO_HANDLE_ERROR + OUT_RESULT_VAR proc_result) + + # If the branch exists locally, check it out. + # Otherwise check it out from origin and create a local branch. + if(proc_result EQUAL 0) + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git checkout ${branch_name} + WORKING_DIRECTORY "${repo_dir}" + ERROR_MESSAGE + "Failed to checkout branch '${branch_name}' in submodule '${submodule_name}'") + else() + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git checkout -b ${branch_name} origin/${branch_name} + WORKING_DIRECTORY "${repo_dir}" + ERROR_MESSAGE + "Failed to checkout branch '${branch_name}' in submodule '${submodule_name}'") + endif() +endfunction() + +# If the update option is set, update the submodules, without fetching. +function(qt_ir_handle_update_option will_checkout_branch working_directory) + set(extra_args "") + if(will_checkout_branch) + list(APPEND extra_args --remote --rebase) + endif() + + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git submodule update --force --no-fetch ${extra_args} + ERROR_MESSAGE "Failed to update submodule '${submodule_name}'" + WORKING_DIRECTORY "${working_directory}") +endfunction() + +# Looks for the 'default' and 'existing' keys, and replaces them with appropriate +# values, while making sure to prepend '-' to the values if the original key had it. +function(qt_ir_handle_dash_in_module_subset_expansion out_var + module_subset already_initialized_submodules) + + set(expanded_module_subset "") + foreach(submodule_name IN LISTS module_subset) + set(has_dash FALSE) + string(REGEX REPLACE "^(-)" "" submodule_name "${submodule_name}") + if(CMAKE_MATCH_1) + set(has_dash TRUE) + endif() + + # Replace the default keyword in the input, with the the list of default submodules types, + # which will be further replaced. + if(submodule_name STREQUAL "default") + set(replacement "preview;essential;addon;deprecated") + # Replace the existing keyword, with the list of already initialized submodules + # from a previous run. + elseif(submodule_name STREQUAL "existing") + set(replacement "${already_initialized_submodules}") + + if(has_dash) + # We can't properly support this with the existing algorithm, because we will + # then exclude it also after dependency resolution, and it can cause an empty list + # of submodules in certain situations. + message(FATAL_ERROR "Excluding existing submodules with '-existing' " + "is not supported, just don't include them.") + endif() + else() + set(replacement "${submodule_name}") + endif() + + # Prepend dash to all expanded values + if(has_dash) + list(TRANSFORM replacement PREPEND "-") + endif() + + list(APPEND expanded_module_subset "${replacement}") + endforeach() + + set(${out_var} "${expanded_module_subset}" PARENT_SCOPE) +endfunction() + +# Processes the given module subset using values that were set by parsing the .gitmodules file. +# +# The module subset is a comma-separated list of module names, with an optional '-' at the start. +# If a - is present, the module (or special expanded keyword) is excluded from the subset. +# If the value is empty, the default subset is used on initial runs, or the previously +# existing submodules are used on subsequent runs. +# If the value is "all", all known submodules are included. +# If the value is a status like 'addon' or 'essential', only submodules with that status are +# included. +# If the value is 'existing', only submodules that were previously initialized are included. +# This evaluates to an empty list for the first script run. +# If the value is a module name, only that module is included. +# The modules to exclude are also set separately, so they can be excluded even after dependency +# resolution which is done later. +function(qt_ir_process_module_subset_values prefix) + set(options + PREVIOUSLY_INITIALIZED + ) + set(oneValueArgs + OUT_VAR_INCLUDE + OUT_VAR_EXCLUDE + ) + set(multiValueArgs + ALREADY_INITIALIZED_SUBMODULES + EXTRA_IMPLICIT_SUBMODULES + MODULE_SUBSET + ) + cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + string(REPLACE "," ";" module_subset "${arg_MODULE_SUBSET}") + + # If a module subset is not specified, either use the default list for the very first run, + # or use the previously initialized submodules for a subsequent run. + # + # If the are no previously initialized submodules, and 'existing' is specified, default + # to 'default'. This handles the case when someone runs git submodule deinit --all --force, + # where git initrepository.initialized config key is still true, and then runs + # configure -init-submodules. Without defaulting to default, we would end up with an empty + # subset and configure would fail. + if(NOT module_subset) + if(arg_PREVIOUSLY_INITIALIZED) + if(arg_ALREADY_INITIALIZED_SUBMODULES) + set(module_subset "existing") + else() + message(DEBUG "No previously initialized submodules detected even though " + "'existing' was specified, defaulting to 'default'") + set(module_subset "default") + endif() + else() + set(module_subset "default") + endif() + endif() + + + qt_ir_handle_dash_in_module_subset_expansion( + expanded_module_subset "${module_subset}" "${arg_ALREADY_INITIALIZED_SUBMODULES}") + + set(include_modules "") + set(exclude_modules "") + + if(arg_EXTRA_IMPLICIT_SUBMODULES) + list(APPEND include_modules ${arg_EXTRA_IMPLICIT_SUBMODULES}) + endif() + + foreach(value IN LISTS expanded_module_subset ${prefix}_submodules_to_remove) + # An '-' at the start means we should exclude those modules. + string(REGEX REPLACE "^(-)" "" value "${value}") + set(list_op "APPEND") + if(CMAKE_MATCH_1) + set(list_op "REMOVE_ITEM") + endif() + + if(value STREQUAL "all") + list(${list_op} include_modules "${${prefix}_submodules}") + if("${list_op}" STREQUAL "REMOVE_ITEM") + list(APPEND exclude_modules "${${prefix}_submodules}") + endif() + elseif(value IN_LIST ${prefix}_statuses) + list(${list_op} include_modules "${${prefix}_status_${value}_submodules}") + if("${list_op}" STREQUAL "REMOVE_ITEM") + list(APPEND exclude_modules "${${prefix}_status_${value}_submodules}") + endif() + elseif(NOT "${${prefix}_${value}_path}" STREQUAL "") + list(${list_op} include_modules "${value}") + if("${list_op}" STREQUAL "REMOVE_ITEM") + list(APPEND exclude_modules "${value}") + endif() + else() + if(list_op STREQUAL "REMOVE_ITEM") + message(WARNING "Excluding non-existent module: ${value}") + else() + message(FATAL_ERROR + "Invalid module subset specified, module name is non-existent: ${value}") + endif() + endif() + endforeach() + + set(${arg_OUT_VAR_INCLUDE} "${include_modules}" PARENT_SCOPE) + set(${arg_OUT_VAR_EXCLUDE} "${exclude_modules}" PARENT_SCOPE) +endfunction() + +# Sort the modules and add dependencies if dependency resolving is enabled. +function(qt_ir_get_module_subset_including_deps prefix out_var initial_modules) + qt_ir_get_option_value(resolve-deps resolve_deps) + qt_ir_get_option_value(optional-deps include_optional_deps) + if(resolve_deps) + set(exclude_optional_deps "") + if(NOT include_optional_deps) + set(exclude_optional_deps EXCLUDE_OPTIONAL_DEPS) + endif() + + qt_internal_sort_module_dependencies("${initial_modules}" out_repos + ${exclude_optional_deps} + PARSE_GITMODULES + GITMODULES_PREFIX_VAR "${prefix}" + ) + else() + set(out_repos "${initial_modules}") + endif() + + qt_ir_get_option_value(perl-identical-output perl_identical_output_for_tests) + if(NOT perl_identical_output_for_tests) + message(DEBUG "repos that will be initialized after dependency handling: ${out_repos}") + endif() + + set(${out_var} "${out_repos}" PARENT_SCOPE) +endfunction() + +# Check whether init-repository has been run before, perl style. +# We assume that if the submodule qtbase has been initialized, then init-repository has been run. +function(qt_ir_check_if_already_initialized_perl_style out_var_is_initialized working_directory) + set(cmd git config --get submodule.qtbase.url) + + set(extra_args "") + qt_ir_get_option_value(perl-identical-output perl_identical_output_for_tests) + if(perl_identical_output_for_tests) + list(APPEND extra_args FORCE_QUIET) + endif() + + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS ${cmd} + OUT_RESULT_VAR git_result + OUT_OUTPUT_VAR git_output + OUT_ERROR_VAR git_error + ${extra_args} + NO_HANDLE_ERROR + WORKING_DIRECTORY "${working_directory}") + + if(git_result EQUAL 1 AND NOT git_output) + set(is_initialized FALSE) + elseif(git_result EQUAL 0 AND git_output) + set(is_initialized TRUE) + else() + message(FATAL_ERROR "Failed to get result of ${cmd}: ${git_output}") + endif() + + set(${out_var_is_initialized} "${is_initialized}" PARENT_SCOPE) +endfunction() + +# Check whether init-repository has been run before, cmake style. +# Check for the presence of the initrepository.initialized git config key. +function(qt_ir_check_if_already_initialized_cmake_style out_var_is_initialized working_directory) + set(options + FORCE_QUIET + ) + set(oneValueArgs "") + set(multiValueArgs "") + cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + set(cmd git config --get initrepository.initialized) + + set(extra_args "") + if(arg_FORCE_QUIET) + list(APPEND extra_args FORCE_QUIET) + endif() + + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS ${cmd} + OUT_RESULT_VAR git_result + OUT_OUTPUT_VAR git_output + OUT_ERROR_VAR git_error + ${extra_args} + NO_HANDLE_ERROR + WORKING_DIRECTORY "${working_directory}") + + if(git_result EQUAL 1 AND NOT git_output) + set(is_initialized FALSE) + elseif(git_result EQUAL 0 AND git_output) + set(is_initialized TRUE) + else() + message(FATAL_ERROR "Failed to get result of ${cmd}: ${git_output}") + endif() + + set(${out_var_is_initialized} "${is_initialized}" PARENT_SCOPE) +endfunction() + +# Check whether init-repository has been run before. +# The CMake and perl script do it differently, choose which way to do it based +# on the active options. +function(qt_ir_check_if_already_initialized out_var_is_initialized working_directory) + qt_ir_get_option_value(perl-init-check perl_init_check) + if(perl_init_check) + qt_ir_check_if_already_initialized_perl_style(is_initialized "${working_directory}") + else() + qt_ir_check_if_already_initialized_cmake_style(is_initialized "${working_directory}") + endif() + + set(${out_var_is_initialized} "${is_initialized}" PARENT_SCOPE) +endfunction() + +# Marks the repository as initialized. +# The perl script used to determine this by checking whether the qtbase submodule was initialized. +# In the CMake script, we instead opt to set an explicit marker in the repository. +function(qt_ir_set_is_initialized working_directory) + # If emulating perl style initialization check, don't set the marker and exit early. + qt_ir_get_option_value(perl-init-check perl_init_check) + if(perl_init_check) + return() + endif() + + set(cmd git config initrepository.initialized true) + + set(extra_args "") + qt_ir_get_option_value(perl-identical-output perl_identical_output_for_tests) + if(perl_identical_output_for_tests) + list(APPEND extra_args FORCE_QUIET) + endif() + + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS ${cmd} + ERROR_MESSAGE "Failed to mark repository as initialized" + ${extra_args} + WORKING_DIRECTORY "${working_directory}") +endfunction() + +# If the repository has already been initialized, exit early. +function(qt_ir_handle_if_already_initialized out_var_should_exit working_directory) + set(should_exit FALSE) + + qt_ir_check_if_already_initialized(is_initialized "${working_directory}") + qt_ir_get_option_value(force force) + qt_ir_get_option_value(quiet quiet) + qt_ir_is_called_from_configure(is_called_from_configure) + + if(is_initialized) + if(NOT force) + set(should_exit TRUE) + if(NOT quiet AND NOT is_called_from_configure) + message( + "Will not reinitialize already initialized repository (use -f to force)!") + endif() + endif() + endif() + + set(${out_var_should_exit} ${should_exit} PARENT_SCOPE) +endfunction() + +# Parses git remote.origin.url and extracts the base url and the repository name. +# +# base_url example: git://code.qt.io/qt +# repo name example: qt5 or tqtc-qt5 +function(qt_ir_get_qt5_repo_name_and_base_url) + set(options "") + set(oneValueArgs + OUT_VAR_QT5_REPO_NAME + OUT_VAR_BASE_URL + WORKING_DIRECTORY + ) + set(multiValueArgs "") + cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT arg_WORKING_DIRECTORY) + message(FATAL_ERROR "qt_ir_get_qt5_repo_name_and_base_url: No working directory specified") + endif() + set(working_directory "${arg_WORKING_DIRECTORY}") + + qt_ir_get_option_value(perl-identical-output perl_identical_output_for_tests) + + set(extra_args "") + if(perl_identical_output_for_tests) + set(extra_args FORCE_QUIET) + endif() + + qt_ir_execute_process_and_log_and_handle_error( + COMMAND_ARGS git config remote.origin.url ${extra_args} + ERROR_MESSAGE "No origin remote found for qt5 repository" + OUT_OUTPUT_VAR git_output + WORKING_DIRECTORY "${working_directory}") + + string(STRIP "${git_output}" git_output) + + # Remove the .git at the end, with an optional slash + string(REGEX REPLACE ".git/?$" "" qt5_repo_name "${git_output}") + + # Remove the tqtc- prefix, if it exists, and the qt5 suffix and that will be the base_url + # The qt5_repo_name is qt5 or tqtc-qt5. + string(REGEX REPLACE "((tqtc-)?qt5)$" "" base_url "${qt5_repo_name}") + set(qt5_repo_name "${CMAKE_MATCH_1}") + + if(NOT qt5_repo_name) + set(qt5_repo_name "qt5") + endif() + + if(NOT base_url) + message(FATAL_ERROR "Failed to parse base url from origin remote: ${git_output}") + endif() + + set(${arg_OUT_VAR_QT5_REPO_NAME} "${qt5_repo_name}" PARENT_SCOPE) + set(${arg_OUT_VAR_BASE_URL} "${base_url}" PARENT_SCOPE) +endfunction() + +# Creates a symlink or a forwarding script to the target path. +# Use for setting up git hooks. +function(qt_ir_ensure_link source_path target_path) + qt_ir_get_option_value(force-hooks force_hooks) + if(EXISTS "${target_path}" AND NOT force_hooks) + return() + endif() + + # In case we have a dead symlink or pre-existing hook + file(REMOVE "${target_path}") + + qt_ir_get_option_value(quiet quiet) + if(NOT quiet) + message("Aliasing ${source_path}\n as ${target_path} ...") + endif() + + if(NOT CMAKE_HOST_WIN32) + file(CREATE_LINK "${source_path}" "${target_path}" RESULT result SYMBOLIC) + # Don't continue upon success. If symlinking failed, fallthrough to creating + # a forwarding script. + if(result EQUAL 0) + return() + endif() + endif() + + # Windows doesn't do (proper) symlinks. As the post_commit script needs + # them to locate itself, we write a forwarding script instead. + + # Make the path palatable for MSYS. + string(REGEX REPLACE "^(.):/" "/\\1/" source_path "${source_path}") + + set(contents "#!/bin/sh\nexec ${source_path} \"$@\"\n") + file(WRITE "${target_path}" "${contents}") +endfunction() + +# Installs the git hooks from the qtrepotools module. +function(qt_ir_install_git_hooks) + set(options "") + set(oneValueArgs + PARENT_REPO_BASE_GIT_PATH + TOP_LEVEL_SRC_PATH + WORKING_DIRECTORY + ) + set(multiValueArgs "") + cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT arg_WORKING_DIRECTORY) + message(FATAL_ERROR "qt_ir_install_git_hooks: No working directory specified") + endif() + set(working_directory "${arg_WORKING_DIRECTORY}") + + if(NOT arg_PARENT_REPO_BASE_GIT_PATH) + message(FATAL_ERROR "qt_ir_install_git_hooks: No PARENT_REPO_BASE_GIT_PATH specified") + endif() + set(parent_repo_base_git_path "${arg_PARENT_REPO_BASE_GIT_PATH}") + + if(NOT arg_TOP_LEVEL_SRC_PATH) + message(FATAL_ERROR "qt_ir_install_git_hooks: No TOP_LEVEL_SRC_PATH specified") + endif() + set(top_level_src_path "${arg_TOP_LEVEL_SRC_PATH}") + + set(hooks_dir "${top_level_src_path}/qtrepotools/git-hooks") + if(NOT EXISTS "${hooks_dir}") + message("Warning: cannot find Git hooks, qtrepotools module might be absent") + return() + endif() + + set(prefix ir_hooks) + qt_ir_parse_git_config_file_contents("${prefix}" + READ_GIT_CONFIG_LOCAL + PARENT_REPO_BASE_GIT_PATH "${parent_repo_base_git_path}" + WORKING_DIRECTORY "${working_directory}" + ) + + foreach(submodule_name IN LISTS ${prefix}_submodules) + set(submodule_git_dir "${working_directory}/${submodule_name}/.git") + if(NOT IS_DIRECTORY "${submodule_git_dir}") + # Get first line + file(STRINGS "${submodule_git_dir}" submodule_git_dir_contents LIMIT_COUNT 1) + + # Remove the gitdir: prefix + string(REGEX REPLACE "^(gitdir: )" "" submodule_git_dir + "${submodule_git_dir_contents}") + if("${CMAKE_MATCH_1}" STREQUAL "") + message(FATAL_ERROR "Malformed .git file ${submodule_git_dir}") + endif() + + # Make it an absolute path, because gitdir: is usually relative to the submodule + get_filename_component(submodule_git_dir "${submodule_git_dir}" + ABSOLUTE BASE_DIR "${working_directory}/${submodule_name}") + + # Untested + set(common_dir "${submodule_git_dir}/commondir") + if(EXISTS "${common_dir}") + file(STRINGS "${common_dir}" common_dir_contents LIMIT_COUNT 1) + string(STRIP "${common_dir_contents}" common_dir_path) + set(submodule_git_dir "${submodule_git_dir}/${common_dir_path}") + get_filename_component(submodule_git_dir "${submodule_git_dir}" ABSOLUTE) + endif() + endif() + qt_ir_ensure_link("${hooks_dir}/gerrit_commit_msg_hook" + "${submodule_git_dir}/hooks/commit-msg") + qt_ir_ensure_link("${hooks_dir}/git_post_commit_hook" + "${submodule_git_dir}/hooks/post-commit") + qt_ir_ensure_link("${hooks_dir}/clang-format-pre-commit" + "${submodule_git_dir}/hooks/pre-commit") + endforeach() +endfunction() + +# Saves the list of top-level submodules that should be included and excluded. +# Will be used to pass these values to the top-level configure script. +function(qt_ir_set_top_level_submodules included_submodules excluded_submodules) + set_property(GLOBAL PROPERTY _qt_ir_top_level_included_submodules "${included_submodules}") + set_property(GLOBAL PROPERTY _qt_ir_top_level_excluded_submodules "${excluded_submodules}") +endfunction() + +# Gets the list of top-level submodules that should be included and excluded. +function(qt_ir_get_top_level_submodules out_included_submodules out_excluded_submodules) + get_property(included GLOBAL PROPERTY _qt_ir_top_level_included_submodules) + get_property(excluded GLOBAL PROPERTY _qt_ir_top_level_excluded_submodules) + + set(${out_included_submodules} "${included}" PARENT_SCOPE) + set(${out_excluded_submodules} "${excluded}" PARENT_SCOPE) +endfunction() + +# Parses the .gitmodules file and proceses the submodules based on the module-subset option +# or the given SUBMODULES argument. +# Also adds dependencies if requested. +# +# This is a macro because we want the variables set by +# qt_ir_parse_gitmodules_file_contents to be available in the calling scope, because it's +# essentially setting a dictionarty, and we don't want to propagate all the variables manually. +macro(qt_ir_get_submodules prefix out_var_submodules) + set(options + PREVIOUSLY_INITIALIZED + PROCESS_SUBMODULES_FROM_COMMAND_LINE + ) + set(oneValueArgs + PARENT_REPO_BASE_GIT_PATH + WORKING_DIRECTORY + ) + set(multiValueArgs + ALREADY_INITIALIZED_SUBMODULES + SUBMODULES + ) + cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + qt_ir_get_working_directory_from_arg(working_directory) + + # Parse the .gitmodules content here, so the parsed data is available downstream + # in other functions and recursive calls of the same function. + qt_ir_parse_git_config_file_contents("${prefix}" + READ_GITMODULES + PARENT_REPO_BASE_GIT_PATH "${arg_PARENT_REPO_BASE_GIT_PATH}" + WORKING_DIRECTORY "${working_directory}" + ) + + qt_ir_get_option_value(perl-identical-output perl_identical_output_for_tests) + set(extra_implict_submodules "") + + # Get which modules should be initialized, based on the module-subset option. + if(arg_PROCESS_SUBMODULES_FROM_COMMAND_LINE) + qt_ir_get_option_value(module-subset initial_module_subset) + + # Implicitly add qtrepotools, so we can install git hooks and don't get the + # missing qtrepotools warning. + if(NOT perl_identical_output_for_tests) + list(APPEND extra_implict_submodules "qtrepotools") + qt_ir_is_verbose(verbose) + if(verbose) + message("Implicitly adding qtrepotools to the list of submodules " + "to initialize for access to git commit hooks, etc. " + "(use --module-subset=<values>,-qtrepotools to exclude it)") + endif() + endif() + + if(NOT perl_identical_output_for_tests) + message(DEBUG "module-subset from command line: ${initial_module_subset}") + endif() + elseif(arg_SUBMODULES) + set(initial_module_subset "${arg_SUBMODULES}") + if(NOT perl_identical_output_for_tests) + message(DEBUG "module-subset from args: ${initial_module_subset}") + endif() + else() + message(FATAL_ERROR "No submodules specified") + endif() + + qt_ir_get_cmake_flag(PREVIOUSLY_INITIALIZED previously_initialized_opt) + qt_ir_process_module_subset_values("${prefix}" + ${previously_initialized_opt} + ${perl_identical_output_opt} + ALREADY_INITIALIZED_SUBMODULES ${arg_ALREADY_INITIALIZED_SUBMODULES} + EXTRA_IMPLICIT_SUBMODULES ${extra_implict_submodules} + MODULE_SUBSET "${initial_module_subset}" + OUT_VAR_INCLUDE processed_module_subset + OUT_VAR_EXCLUDE modules_to_exclude + ) + if(NOT perl_identical_output_for_tests) + message(DEBUG "Processed module subset: ${processed_module_subset}") + endif() + + # We only resolve dependencies for the top-level call, not for recursive calls. + if(arg_PROCESS_SUBMODULES_FROM_COMMAND_LINE) + # Resolve which submodules should be initialized, including dependencies. + qt_ir_get_module_subset_including_deps("${prefix}" + submodules_with_deps "${processed_module_subset}") + + # Then remove any explicitly specified submodules. + set(submodules_with_deps_and_excluded "${submodules_with_deps}") + if(modules_to_exclude) + list(REMOVE_ITEM submodules_with_deps_and_excluded ${modules_to_exclude}) + endif() + + if(NOT perl_identical_output_for_tests AND modules_to_exclude) + message(DEBUG "Repos that will be excluded after dependency handling: ${modules_to_exclude}") + endif() + + set(submodules "${submodules_with_deps_and_excluded}") + qt_ir_set_top_level_submodules("${submodules}" "${modules_to_exclude}") + else() + set(submodules "${processed_module_subset}") + endif() + + # Remove duplicates + set(submodules_maybe_duplicates "${submodules}") + list(REMOVE_DUPLICATES submodules) + if(NOT perl_identical_output_for_tests AND NOT submodules STREQUAL submodules_maybe_duplicates) + message(DEBUG "Removed duplicates from submodules, final list: ${submodules}") + endif() + + set(${out_var_submodules} "${submodules}" PARENT_SCOPE) +endmacro() + +# Recursively initialize submodules starting from the given current working directory. +# This is the equivalent of the perl script's git_clone_all_submodules function. +function(qt_ir_handle_init_submodules prefix) + set(options + CHECKOUT_BRANCH + PREVIOUSLY_INITIALIZED + PROCESS_SUBMODULES_FROM_COMMAND_LINE + ) + set(oneValueArgs + ALTERNATES + BASE_URL + PARENT_REPO_BASE_GIT_PATH + WORKING_DIRECTORY + ) + set(multiValueArgs + ALREADY_INITIALIZED_SUBMODULES + SUBMODULES + ) + cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + qt_ir_get_working_directory_from_arg(working_directory) + + # Get the submodules that should be initialized. + qt_ir_get_cmake_flag(PROCESS_SUBMODULES_FROM_COMMAND_LINE + process_submodules_from_command_line_opt) + qt_ir_get_cmake_flag(PREVIOUSLY_INITIALIZED + previously_initialized_opt) + qt_ir_get_submodules(${prefix} submodules + ${process_submodules_from_command_line_opt} + ${previously_initialized_opt} + ALREADY_INITIALIZED_SUBMODULES ${arg_ALREADY_INITIALIZED_SUBMODULES} + PARENT_REPO_BASE_GIT_PATH "${arg_PARENT_REPO_BASE_GIT_PATH}" + SUBMODULES "${arg_SUBMODULES}" + WORKING_DIRECTORY "${working_directory}" + ) + + qt_ir_get_option_value(perl-identical-output perl_identical_output_for_tests) + if(NOT submodules AND NOT perl_identical_output_for_tests) + message("No submodules were given to initialize or they were all excluded.") + return() + endif() + + # Initialize the submodules, but don't clone them yet. + qt_ir_run_git_submodule_init("${submodules}" "${working_directory}") + + # Deinit submodules that are not in the list of submodules to be initialized. + qt_ir_handle_submodule_removal_and_ignoring("${prefix}" + "${submodules}" "${arg_PARENT_REPO_BASE_GIT_PATH}" "${working_directory}") + + # Check for dirty submodules. + qt_ir_handle_dirty_submodule("${submodules}" "${working_directory}") + + qt_ir_get_cmake_flag(CHECKOUT_BRANCH branch_flag) + qt_ir_get_option_as_cmake_flag_option(fetch "FETCH" fetch_flag) + + # Manually clone each submodule if it was not previously cloned, so we can easily + # use reference (alternates) repos, mirrors, etc. + # If already cloned, just fetch new data. + # + # Note that manually cloning the submodules, as opposed to running git submodule update, + # places the .git directories inside the submodule directories, but latest git versions + # expect it in $super_repo/.git/modules. + # When de-initializing submodules manually, git will absorb the .git directories into the super + # repo. + # In case if the super repo already has a copy of the submodule .git dir, git will fail + # to absorb the .git dir and error out. In that case the already existing .git dir needs to be + # removed manually, there is no git command to do it afaik. + foreach(submodule_name IN LISTS submodules) + qt_ir_clone_one_submodule(${submodule_name} + ALTERNATES ${arg_ALTERNATES} + BASE_URL ${arg_BASE_URL} + WORKING_DIRECTORY "${working_directory}" + ${branch_flag} + ${fetch_flag} + ) + endforeach() + + # Checkout branches instead of the default detached HEAD. + if(branch_flag) + foreach(submodule_name IN LISTS submodules) + qt_ir_handle_branch_option("${prefix}" ${submodule_name} "${working_directory}") + endforeach() + endif() + + qt_ir_get_option_value(update will_update) + if(will_update) + + # Update the checked out refs without fetching. + qt_ir_handle_update_option("${branch_flag}" "${working_directory}") + + # Recursively initialize submodules of submodules. + foreach(submodule_name IN LISTS submodules) + set(submodule_path "${${prefix}_${submodule_name}_path}") + set(submodule_gitmodules_path "${working_directory}/${submodule_path}/.gitmodules") + + if(EXISTS "${submodule_gitmodules_path}") + set(alternates_option "") + if(arg_ALTERNATES) + set(alternates_option ALTERNATES "${arg_ALTERNATES}/${submodule_name}") + endif() + + set(submodule_base_git_path "${${prefix}_${submodule_name}_base_git_path}") + + qt_ir_handle_init_submodules( + # Use a different prefix to store new gitmodules data + ir_sub_${submodule_name} + + # Check out all submodules recursively + SUBMODULES "all" + + BASE_URL "${base_url}" + PARENT_REPO_BASE_GIT_PATH "${submodule_base_git_path}" + WORKING_DIRECTORY "${working_directory}/${submodule_name}" + + # The CHECKOUT_BRANCH option is not propagated on purpose + ${alternates_option} + ) + endif() + endforeach() + endif() +endfunction() |