From 29019c9caee083779d1db1ab1d487594cd835735 Mon Sep 17 00:00:00 2001 From: Alexandru Croitor Date: Fri, 15 Dec 2023 11:22:55 +0100 Subject: [PATCH] CMake: Rewrite init-repository using CMake and .sh / .bat scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit init-repository is now implemented using CMake + .sh / .bat scripts. The intent behind the change is not to require Perl to checkout and build Qt, because it can be troublesome to acquire on Windows and it can also lead to issues during builds due to CMake picking up a Perl distribution-shipped compiler. All previous options were ported over like - module-subset - alternates - etc. A few new options were added: - --resolve-deps / --no-resolve-deps - --optional-deps / --no-optional-deps - --verbose and some other internal ones for testing reasons. The new script does automatic resolving of dependencies based on the depends / recommends keys in .gitmodules unless --no-resolve-deps is passed. So if you configure with --module-subset=qtsvg, the script will also initialize qtbase. If --no-optional-deps is passed, only required dependencies ('depends' ky) will be included and optional dependencies ('recommends' key) will be excluded. The new script now has a new default behavior when calling init-repository a second time with --force, without specifying a --module-subset option. Instead of initializing all submodules, it will just update the existing / previously initialized submodules. It also understands a new module-subset keyword "existing", which expands to the previously initialized submodules, so someone can initialize an additional submodule by calling init-repository -f --module-subset=existing,qtsvg Implementation notes: The overall code flow is init-repository -> cmake/QtIRScript.cmake -> qt_ir_run_main_script -> qt_ir_run_after_args_parsed -> qt_ir_handle_init_submodules (recursive) -> qt_ir_clone_one_submodule with some bells and whistles on the side. The command line parsing is an adapted copy of the functions in qtbase/cmake/QtProcessConfigureArgs.cmake. We can't use those exact functions because qtbase is not available when init-repository is initially called, and force cloning qtbase was deemed undesirable. We also have a new mechanism to detect whether init-repository was previously called. The perl script used the existence of the qtbase submodule as the check. In the cmake script, we instead set a custom marker into the local repo config file. Otherwise the code logic should be a faithful reimplementation of init-repository.pl aside from some small things like logging and progress reporting. The pre-existing git cloning logic in QtTopLevelHelpers was not used because it would not be compatible with the alternates option and I didn't want to accidentally break the pre-existing code. Plus init-repository is a bit opinionated about how it clones and checks out repos. The dependency collection and sorting logic uses the pre-existing code though. See follow up commit about implicitly calling init-repository when qt5/configure is called and the repo was not initialized before. [ChangeLog][General] init-repository was rewritten using CMake. Perl is no longer required to initialize the qt5.git super repo. Task-number: QTBUG-120030 Task-number: QTBUG-122622 Change-Id: Ibc38ab79d3fdedd62111ebbec496eabd64c20d2b Reviewed-by: Alexey Edelev Reviewed-by: Joerg Bornemann Reviewed-by: Tor Arne Vestbø --- .gitignore | 2 + cmake/QtIRCommandLineHelpers.cmake | 369 +++++++++ cmake/QtIRGitHelpers.cmake | 1113 ++++++++++++++++++++++++++++ cmake/QtIRHelp.txt | 132 ++++ cmake/QtIRHelpers.cmake | 158 ++++ cmake/QtIROptionsHelpers.cmake | 34 + cmake/QtIRParsingHelpers.cmake | 237 ++++++ cmake/QtIRProcessHelpers.cmake | 165 +++++ cmake/QtIRScript.cmake | 17 + cmake/QtTopLevelHelpers.cmake | 104 ++- cmake/QtWriteArgsFile.cmake | 92 +++ init-repository | 24 + init-repository.bat | 23 + 13 files changed, 2460 insertions(+), 10 deletions(-) create mode 100644 cmake/QtIRCommandLineHelpers.cmake create mode 100644 cmake/QtIRGitHelpers.cmake create mode 100644 cmake/QtIRHelp.txt create mode 100644 cmake/QtIRHelpers.cmake create mode 100644 cmake/QtIROptionsHelpers.cmake create mode 100644 cmake/QtIRParsingHelpers.cmake create mode 100644 cmake/QtIRProcessHelpers.cmake create mode 100644 cmake/QtIRScript.cmake create mode 100644 cmake/QtWriteArgsFile.cmake create mode 100755 init-repository create mode 100644 init-repository.bat diff --git a/.gitignore b/.gitignore index d4d4cae1..30b03896 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ CMakeUserPresets.json build build-* .DS_Store +init-repository.opt +init-repository.opt.in diff --git a/cmake/QtIRCommandLineHelpers.cmake b/cmake/QtIRCommandLineHelpers.cmake new file mode 100644 index 00000000..5cc0caed --- /dev/null +++ b/cmake/QtIRCommandLineHelpers.cmake @@ -0,0 +1,369 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# This file contains a modified subset of the qtbase/QtProcessConfigureArgs.cmake commands +# with renamed functions, because we need similar logic for init-repository, but +# we can't access qtbase before we clone it. + +# Call a function with the given arguments. +function(qt_ir_call_function func) + set(call_code "${func}(") + math(EXPR n "${ARGC} - 1") + foreach(i RANGE 1 ${n}) + string(APPEND call_code "\"${ARGV${i}}\" ") + endforeach() + string(APPEND call_code ")") + string(REPLACE "\\" "\\\\" call_code "${call_code}") + if(${CMAKE_VERSION} VERSION_LESS "3.18.0") + set(incfile qt_tmp_func_call.cmake) + file(WRITE "${incfile}" "${call_code}") + include(${incfile}) + file(REMOVE "${incfile}") + else() + cmake_language(EVAL CODE "${call_code}") + endif() +endfunction() + +# Show an error. +function(qt_ir_add_error) + message(FATAL_ERROR ${ARGV}) +endfunction() + +# Check if there are still unhandled command line arguments. +function(qt_ir_args_has_next_command_line_arg out_var) + qt_ir_get_unhandled_args(args) + + list(LENGTH args n) + if(n GREATER 0) + set(result TRUE) + else() + set(result FALSE) + endif() + set(${out_var} ${result} PARENT_SCOPE) +endfunction() + +# Get the next unhandled command line argument without popping it. +function(qt_ir_args_peek_next_command_line_arg out_var) + qt_ir_get_unhandled_args(args) + list(GET args 0 result) + set(${out_var} ${result} PARENT_SCOPE) +endfunction() + +# Get the next unhandled command line argument. +function(qt_ir_args_get_next_command_line_arg out_var) + qt_ir_get_unhandled_args(args) + list(POP_FRONT args result) + qt_ir_set_unhandled_args("${args}") + set(${out_var} ${result} PARENT_SCOPE) +endfunction() + +# Helper macro to parse the arguments for the command line options. +macro(qt_ir_commandline_option_parse_arguments) + set(options UNSUPPORTED) + set(oneValueArgs TYPE NAME SHORT_NAME ALIAS VALUE DEFAULT_VALUE) + set(multiValueArgs VALUES MAPPING) + cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) +endmacro() + +# We use this to define the command line options that init-repository accepts. +# Arguments +# name - name of the long form option +# e.g. 'module-subset' will parse '--module-subset' +# UNSUPPORTED - mark the option as unsupported in the cmake port of init-repository, +# which means we will fall back to calling the perl script instead +# TYPE - the type of the option, currently we support boolean, string and void +# VALUE - the value to be set for a 'void' type option +# VALUES - the valid values for an option +# MAPPING - currently unused +# SHORT_NAME - an alternative short name flag, +# e.g. 'f' will parse -f for --force +# ALIAS - mark the option as an alias of another option, both will have the +# same value when retrieved. +# DEFAULT_VALUE - the default value to be set for the option when it's not specified +# on the command line +# +# NOTE: Make sure to update the SHORT_NAME code path when adding new options. +function(qt_ir_commandline_option_helper name) + qt_ir_commandline_option_parse_arguments(${ARGN}) + + set(unsupported_options "${commandline_known_unsupported_options}") + if(arg_UNSUPPORTED) + set(commandline_option_${name}_unsupported + "${arg_UNSUPPORTED}" PARENT_SCOPE) + list(APPEND unsupported_options "${name}") + endif() + set(commandline_known_unsupported_options "${unsupported_options}" PARENT_SCOPE) + + set(commandline_known_options + "${commandline_known_options};${name}" PARENT_SCOPE) + + set(commandline_option_${name}_type "${arg_TYPE}" PARENT_SCOPE) + + if(NOT "${arg_VALUE}" STREQUAL "") + set(commandline_option_${name}_value "${arg_VALUE}" PARENT_SCOPE) + endif() + + if(arg_VALUES) + set(commandline_option_${name}_values ${arg_VALUES} PARENT_SCOPE) + elseif(arg_MAPPING) + set(commandline_option_${name}_mapping ${arg_MAPPING} PARENT_SCOPE) + endif() + + if(NOT "${arg_SHORT_NAME}" STREQUAL "") + set(commandline_option_${name}_short_name "${arg_SHORT_NAME}" PARENT_SCOPE) + endif() + + if(NOT "${arg_ALIAS}" STREQUAL "") + set(commandline_option_${name}_alias "${arg_ALIAS}" PARENT_SCOPE) + endif() + + # Should be last, in case alias was specified + if(NOT "${arg_DEFAULT_VALUE}" STREQUAL "") + set(commandline_option_${name}_default_value "${arg_DEFAULT_VALUE}" PARENT_SCOPE) + qt_ir_command_line_set_input("${name}" "${arg_DEFAULT_VALUE}") + endif() +endfunction() + +# Defines an option that init-repository understands. +# Uses qt_ir_commandline_option_helper to define both long and short option names. +macro(qt_ir_commandline_option name) + # Define the main option + qt_ir_commandline_option_helper("${name}" ${ARGN}) + + qt_ir_commandline_option_parse_arguments(${ARGN}) + + # Define the short name option if it's requested + if(NOT "${arg_SHORT_NAME}" STREQUAL "" + AND "${commandline_option_${arg_SHORT_NAME}_type}" STREQUAL "") + set(unsupported "") + if(arg_UNSUPPORTED) + set(unsupported "${arg_UNSUPPORTED}") + endif() + + qt_ir_commandline_option_helper("${arg_SHORT_NAME}" + TYPE "${arg_TYPE}" + ALIAS "${name}" + VALUE "${arg_VALUE}" + VALUES ${arg_VALUES} + MAPPING ${arg_MAPPING} + DEFAULT_VALUE ${arg_DEFAULT_VALUE} + ${unsupported} + ) + endif() +endmacro() + +# Saves the value of a command line option into a global property. +function(qt_ir_command_line_set_input name val) + if(NOT "${commandline_option_${name}_alias}" STREQUAL "") + set(name "${commandline_option_${name}_alias}") + endif() + + set_property(GLOBAL PROPERTY _qt_ir_input_${name} "${val}") + set_property(GLOBAL APPEND PROPERTY _qt_ir_inputs ${name}) +endfunction() + +# Appends a value of a command line option into a global property. +# Currently unused +function(qt_ir_command_line_append_input name val) + if(NOT "${commandline_option_${name}_alias}" STREQUAL "") + set(name "${commandline_option_${name}_alias}") + endif() + + get_property(oldval GLOBAL PROPERTY _qt_ir_input_${name}) + if(NOT "${oldval}" STREQUAL "") + string(PREPEND val "${oldval};") + endif() + qt_ir_command_line_set_input(${name} "${val}" ) +endfunction() + +# Checks if the value of a command line option is valid. +function(qt_ir_validate_value opt val out_var) + set(${out_var} TRUE PARENT_SCOPE) + + set(valid_values ${commandline_option_${arg}_values}) + list(LENGTH valid_values n) + if(n EQUAL 0) + return() + endif() + + foreach(v ${valid_values}) + if(val STREQUAL v) + return() + endif() + endforeach() + + set(${out_var} FALSE PARENT_SCOPE) + list(JOIN valid_values " " valid_values_str) + qt_ir_add_error("Invalid value '${val}' supplied to command line option '${opt}'." + "\nAllowed values: ${valid_values_str}\n") +endfunction() + +# Sets / handles the value of a command line boolean option. +function(qt_ir_commandline_boolean arg val nextok) + if("${val}" STREQUAL "") + set(val "yes") + endif() + if(NOT val STREQUAL "yes" AND NOT val STREQUAL "no") + message(FATAL_ERROR + "Invalid value '${val}' given for boolean command line option '${arg}'.") + endif() + qt_ir_command_line_set_input("${arg}" "${val}") +endfunction() + +# Sets / handles the value of a command line string option. +function(qt_ir_commandline_string arg val nextok) + if(nextok) + qt_ir_args_get_next_command_line_arg(val) + + if("${val}" MATCHES "^-") + qt_ir_add_error("No value supplied to command line options '${arg}'.") + endif() + endif() + qt_ir_validate_value("${arg}" "${val}" success) + if(success) + qt_ir_command_line_set_input("${arg}" "${val}") + endif() +endfunction() + +# Sets / handles the value of a command line void option. +# This is an option like --force, which doesn't take any arguments. +# Currently unused +function(qt_ir_commandline_void arg val nextok) + if(NOT "${val}" STREQUAL "") + qt_i_add_error("Command line option '${arg}' expects no argument ('${val}' given).") + endif() + if(DEFINED commandline_option_${arg}_value) + set(val ${commandline_option_${arg}_value}) + endif() + if("${val}" STREQUAL "") + set(val yes) + endif() + qt_ir_command_line_set_input("${arg}" "${val}") +endfunction() + +# Reads the command line arguments from the optfile_path. +function(qt_ir_get_raw_args_from_optfile optfile_path out_var) + file(STRINGS "${optfile_path}" args) + set(${out_var} "${args}" PARENT_SCOPE) +endfunction() + + # Reads the optfile_path, iterates over the given command line arguments, + # sets the input for recongized options. + # + # IGNORE_UNKNOWN_ARGS tells the function not to fail if it encounters an unknown + # option, needed when the script is called from the configure script with + # configure-only-known options. +function(qt_ir_process_args_from_optfile optfile_path) + set(options IGNORE_UNKNOWN_ARGS) + set(oneValueArgs "") + set(multiValueArgs "") + cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + qt_ir_get_raw_args_from_optfile("${optfile_path}" configure_args) + qt_ir_set_unhandled_args("${configure_args}") + + while(1) + qt_ir_args_has_next_command_line_arg(has_next) + if(NOT has_next) + break() + endif() + qt_ir_args_get_next_command_line_arg(arg) + + # parse out opt and val + set(nextok FALSE) + if(arg MATCHES "^--?(disable|no)-(.*)") + set(opt "${CMAKE_MATCH_2}") + set(val "no") + elseif(arg MATCHES "^--([^=]+)=(.*)") + set(opt "${CMAKE_MATCH_1}") + set(val "${CMAKE_MATCH_2}") + elseif(arg MATCHES "^--(.*)") + set(nextok TRUE) + set(opt "${CMAKE_MATCH_1}") + unset(val) + elseif(arg MATCHES "^-(.*)") + set(nextok TRUE) + set(opt "${CMAKE_MATCH_1}") + unset(val) + else() + qt_ir_add_error("Invalid command line parameter '${arg}'.") + endif() + + set(type "${commandline_option_${opt}_type}") + + if("${type}" STREQUAL "") + if(NOT arg_IGNORE_UNKNOWN_ARGS) + qt_ir_add_error("Unknown command line option '${arg}'.") + else() + message(DEBUG "Unknown command line option '${arg}'. Ignoring.") + continue() + endif() + endif() + + if(NOT COMMAND "qt_ir_commandline_${type}") + qt_ir_add_error("Unknown type '${type}' for command line option '${opt}'.") + endif() + qt_ir_call_function("qt_ir_commandline_${type}" "${opt}" "${val}" "${nextok}") + endwhile() +endfunction() + +# Shows help for the command line options. +function(qt_ir_show_help) + set(help_file "${CMAKE_CURRENT_LIST_DIR}/QtIRHelp.txt") + if(EXISTS "${help_file}") + file(READ "${help_file}" content) + message("${content}") + endif() + + message([[ +General Options: +-help, -h ............ Display this help screen +]]) +endfunction() + +# Gets the unhandled command line args. +function(qt_ir_get_unhandled_args out_var) + get_property(args GLOBAL PROPERTY _qt_ir_unhandled_args) + set(${out_var} "${args}" PARENT_SCOPE) +endfunction() + +# Sets the unhandled command line args. +function(qt_ir_set_unhandled_args args) + set_property(GLOBAL PROPERTY _qt_ir_unhandled_args "${args}") +endfunction() + +# Gets the unsupported options that init-repository.pl supports, but the cmake port does +# not support. +function(qt_ir_get_unsupported_options out_var) + set(${out_var} "${commandline_known_unsupported_options}" PARENT_SCOPE) +endfunction() + +# Get the value of a command line option. +function(qt_ir_get_option_value name out_var) + if(NOT "${commandline_option_${name}_alias}" STREQUAL "") + set(name "${commandline_option_${name}_alias}") + endif() + + get_property(value GLOBAL PROPERTY _qt_ir_input_${name}) + set(${out_var} "${value}" PARENT_SCOPE) +endfunction() + +# Set the value of a command line option manually. +function(qt_ir_set_option_value name value) + if(NOT "${commandline_option_${name}_alias}" STREQUAL "") + set(name "${commandline_option_${name}_alias}") + endif() + + qt_ir_command_line_set_input("${name}" "${value}") +endfunction() + +# Get the value of a command line option as a cmakke flag option, to be passed +# to functions that use cmake_parse_arguments. +function(qt_ir_get_option_as_cmake_flag_option cli_name cmake_option_name out_var) + qt_ir_get_option_value("${cli_name}" bool_value) + set(cmake_option "") + if(bool_value) + set(cmake_option "${cmake_option_name}") + endif() + set(${out_var} "${cmake_option}" PARENT_SCOPE) +endfunction() diff --git a/cmake/QtIRGitHelpers.cmake b/cmake/QtIRGitHelpers.cmake new file mode 100644 index 00000000..0797ed35 --- /dev/null +++ b/cmake/QtIRGitHelpers.cmake @@ -0,0 +1,1113 @@ +# 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(NOT module_subset) + if(arg_PREVIOUSLY_INITIALIZED) + set(module_subset "existing") + 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) + + if(is_initialized) + if(NOT force) + set(should_exit TRUE) + if(NOT quiet) + 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 + 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}") + + set(hooks_dir "${CMAKE_CURRENT_SOURCE_DIR}/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() + +# 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=,-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}") + list(REMOVE_ITEM submodules_with_deps_and_excluded ${modules_to_exclude}) + + 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}") + 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() diff --git a/cmake/QtIRHelp.txt b/cmake/QtIRHelp.txt new file mode 100644 index 00000000..03b92548 --- /dev/null +++ b/cmake/QtIRHelp.txt @@ -0,0 +1,132 @@ +Usage: + ./init-repository [options] + + This script may be run after an initial `git clone' of the Qt supermodule + in order to check out all submodules. It fetches them from canonical URLs + inferred from the clone's origin. + +Options: + Global options: + + --force, -f + Force initialization (even if the submodules are already checked + out). + + --force-hooks + Force initialization of hooks (even if there are already hooks in + checked out submodules). + + --quiet, -q + Be quiet. Will exit cleanly if the repository is already + initialized. + + --verbose + Adds a bit more output when executing processes + + --no-resolve-deps + By default, each submodule specified via the module-subset option + will have its required and optional dependencies also initialized. + This option can be passed to disable automatic initialization of + dependencies, so that the exact list passed to module-subset is + initialized. + + --no-optional-deps + By default, each submodule specified via the module-subset option + will have its optional dependencies also initialized. + This option can be passed to initialize only required dependencies of + the given module-subset. + + Module options: + + --module-subset=,... + Only initialize the specified subset of modules given as the + argument. Specified modules must already exist in .gitmodules. The + string "all" results in cloning all known modules. The strings + "essential", "addon", "preview", "deprecated", "obsolete", + "additionalLibrary", and "ignore" refer to classes of modules + identified by "status=" lines in the .gitmodules file. + You can use "existing" to to reference already initialized submodules. + Additionally, "qtrepotools" is implicitly always added to ensure + relevant git commit hooks are available. It can be excluded as described + below. + You can use "default" in the subset as a short-hand for + "essential,addon,preview,deprecated", which corresponds to the set of + maintained modules included in standard Qt releases; this is also the + default module subset when this option is not given when first running + init-repositoy. If init-repository is rerun a second time (with --force) + the default is to initialize the "existing" submodules, rather than the + default subset. Entries may be prefixed with a dash to exclude them + from a bigger set, e.g. "all,-ignore" or "existing,-qttools". + + --no-update + Skip the `git submodule update' command. + + --no-fetch + Skip the `git fetch' commands. Implied by --no-update. + + --branch + Instead of checking out specific SHA1s, check out the submodule + branches that correspond with the current supermodule commit. By + default, this option will cause local commits in the submodules to + be rebased. With --no-update, the branches will be checked out, but + their heads will not move. + + --ignore-submodules + Set git config to ignore submodules by default when doing operations + on the qt5 repo, such as `pull', `fetch', `diff' etc. + + After using this option, pass `--ignore-submodules=none' to git to + override it as needed. + + Repository options: + + --berlin + Switch to internal URLs and make use of the Berlin git mirrors. + (Implies `--mirror'). + + --oslo + Switch to internal URLs and make use of the Oslo git mirrors. + (Implies `--mirror'). + + --codereview-username + Specify the user name for the (potentially) writable `gerrit' remote + for each module, for use with the Gerrit code review tool. + + If this option is omitted, the gerrit remote is created without a + username and port number, and thus relies on a correct SSH + configuration. + + --alternates + Adds alternates for each submodule to another full qt5 checkout. + This makes this qt5 checkout very small, as it will use the object + store of the alternates before unique objects are stored in its own + object store. + + This option has no effect when using `--no-update'. + + NOTE: This will make this repo dependent on the alternate, which is + potentially dangerous! The dependency can be broken by also using + the `--copy-objects' option, or by running "git repack -a" in each + submodule, where required. Please read the note about the `--shared' + option in the documentation of `git clone' for more information. + + --copy-objects + When `--alternates' is used, automatically do a "git repack -a" in + each submodule after cloning, to ensure that the repositories are + independent from the source used as a reference for cloning. + + Note that this negates the disk usage benefits gained from the use + of `--alternates'. + --mirror + Uses as the base URL for submodule git mirrors. + + For example: + + --mirror user\@machine:/foo/bar/qt/ + + ...will use the following as a mirror for qtbase: + + user\@machine:/foo/bar/qt/qtbase.git + + The mirror is permitted to contain a subset of the submodules; any + missing modules will fall back to the canonical URLs. diff --git a/cmake/QtIRHelpers.cmake b/cmake/QtIRHelpers.cmake new file mode 100644 index 00000000..18a76180 --- /dev/null +++ b/cmake/QtIRHelpers.cmake @@ -0,0 +1,158 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Includes all helper files for access to necessary functions. +macro(qt_ir_include_all_helpers) + include(QtIRCommandLineHelpers) + include(QtIRGitHelpers) + include(QtIROptionsHelpers) + include(QtIRParsingHelpers) + include(QtIRProcessHelpers) + include(QtTopLevelHelpers) +endmacro() + +# Convenience macro to get the working directory from the arguments passed to +# cmake_parse_arguments. Saves a few lines and makes reading the code slightly +# easier. +macro(qt_ir_get_working_directory_from_arg out_var) + if(NOT arg_WORKING_DIRECTORY) + message(FATAL_ERROR "No working directory specified") + endif() + set(${out_var} "${arg_WORKING_DIRECTORY}") +endmacro() + +# Convenience function to set the variable to the name of cmake_parse_arguments +# flag option if it is active. +function(qt_ir_get_cmake_flag flag_name out_var) + if(arg_${flag_name}) + set(${out_var} "${flag_name}" PARENT_SCOPE) + else() + set(${out_var} "" PARENT_SCOPE) + endif() +endfunction() + +# Checks whether any of the arguments passed on the command line are options +# that are marked as unsupported in the cmake port of init-repository. +function(qt_ir_check_if_unsupported_options_used out_var out_var_option_name) + qt_ir_get_unsupported_options(unsupported_options) + + set(unsupported_options_used FALSE) + foreach(unsupported_option IN LISTS unsupported_options) + qt_ir_get_option_value(${unsupported_option} value) + if(value) + set(${out_var_option_name} "${unsupported_option}" PARENT_SCOPE) + set(unsupported_options_used TRUE) + break() + endif() + endforeach() + set(${out_var} "${unsupported_options_used}" PARENT_SCOPE) +endfunction() + +# When an unsupported option is used, show an error message and tell the user +# to run the perly script manually. +function(qt_ir_show_error_how_to_run_perl opt_file unsupported_option_name) + qt_ir_get_raw_args_from_optfile("${opt_file}" args) + string(REPLACE ";" " " args "${args}") + + set(perl_cmd "perl ./init-repository.pl ${args}") + + message(FATAL_ERROR + "Option '${unsupported_option_name}' is not implemented in the cmake " + "port of init-repository. Please let us know if this option is really " + "important for you at https://bugreports.qt.io/. Meanwhile, you can " + "still run the perl script directly. \n ${perl_cmd}") +endfunction() + +# Check whether help was requested. +function(qt_ir_is_help_requested out_var) + qt_ir_get_option_value(help value) + set(${out_var} "${value}" PARENT_SCOPE) +endfunction() + +# Check whether the verbose option was used. +function(qt_ir_is_verbose out_var) + qt_ir_get_option_value(verbose value) + set(${out_var} "${value}" PARENT_SCOPE) +endfunction() + +# Main logic of the script. +function(qt_ir_run_after_args_parsed) + qt_ir_is_help_requested(show_help) + if(show_help) + qt_ir_show_help() + return() + endif() + + set(working_directory "${CMAKE_CURRENT_SOURCE_DIR}") + + qt_ir_handle_if_already_initialized(should_exit "${working_directory}") + if(should_exit) + return() + endif() + + # This will be used by the module subset processing to determine whether we + # should re-initialize the previously initialized (existing) subset. + qt_ir_check_if_already_initialized_cmake_style(is_initialized + "${working_directory}" FORCE_QUIET) + set(previously_initialized_option "") + if(is_initialized) + set(previously_initialized_option PREVIOUSLY_INITIALIZED) + endif() + + + # Ge the name of the qt5 repo (tqtc- or not) and the base url for all other repos + qt_ir_get_qt5_repo_name_and_base_url( + OUT_VAR_QT5_REPO_NAME qt5_repo_name + OUT_VAR_BASE_URL base_url + WORKING_DIRECTORY "${working_directory}") + + qt_ir_get_already_initialized_submodules("${prefix}" + already_initialized_submodules + "${qt5_repo_name}" + "${working_directory}") + + # Get some additional options to pass down. + qt_ir_get_option_value(alternates alternates) + qt_ir_get_option_as_cmake_flag_option(branch "CHECKOUT_BRANCH" checkout_branch_option) + + # The prefix for the cmake-style 'dictionary' that will be used by various functions. + set(prefix "ir_top") + + # Initialize and clone the submodules + qt_ir_handle_init_submodules("${prefix}" + ALTERNATES "${alternates}" + ALREADY_INITIALIZED_SUBMODULES "${already_initialized_submodules}" + BASE_URL "${base_url}" + PARENT_REPO_BASE_GIT_PATH "${qt5_repo_name}" + PROCESS_SUBMODULES_FROM_COMMAND_LINE + WORKING_DIRECTORY "${working_directory}" + ${checkout_branch_option} + ${previously_initialized_option} + ) + + # Add gerrit remotes. + qt_ir_add_git_remotes("${qt5_repo_name}" "${working_directory}") + + # Install commit and other various hooks. + qt_ir_install_git_hooks( + PARENT_REPO_BASE_GIT_PATH "${qt5_repo_name}" + WORKING_DIRECTORY "${working_directory}" + ) + + # Mark the repo as being initialized. + qt_ir_set_is_initialized("${working_directory}") +endfunction() + +# Entrypoint of the init-repository script. +function(qt_ir_run_main_script) + qt_ir_set_known_command_line_options() + qt_ir_process_args_from_optfile("${OPTFILE}") + + qt_ir_check_if_unsupported_options_used( + unsupported_options_used option_name) + if(unsupported_options_used) + qt_ir_show_error_how_to_run_perl("${OPTFILE}" "${option_name}") + endif() + + qt_ir_run_after_args_parsed() +endfunction() diff --git a/cmake/QtIROptionsHelpers.cmake b/cmake/QtIROptionsHelpers.cmake new file mode 100644 index 00000000..877265be --- /dev/null +++ b/cmake/QtIROptionsHelpers.cmake @@ -0,0 +1,34 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Declare command line options known to init-repository. +macro(qt_ir_set_known_command_line_options) + # Implemented options + + # Note alternates is a qt specific option name, but it uses + # git submodule's --reference option underneath which also implies --shared. + # It essentially uses the git object storage of another repo, to avoid + # cloning the same objects and thus saving space. + qt_ir_commandline_option(alternates TYPE string) + + qt_ir_commandline_option(berlin TYPE boolean) + qt_ir_commandline_option(branch TYPE boolean) + qt_ir_commandline_option(codereview-username TYPE string) + qt_ir_commandline_option(copy-objects TYPE boolean) + qt_ir_commandline_option(fetch TYPE boolean DEFAULT_VALUE yes) + qt_ir_commandline_option(force SHORT_NAME f TYPE boolean) + qt_ir_commandline_option(force-hooks TYPE boolean) + qt_ir_commandline_option(help SHORT_NAME h TYPE boolean) + qt_ir_commandline_option(ignore-submodules TYPE boolean) + qt_ir_commandline_option(mirror TYPE string) + qt_ir_commandline_option(module-subset TYPE string) + qt_ir_commandline_option(optional-deps TYPE boolean DEFAULT_VALUE yes) + qt_ir_commandline_option(oslo TYPE boolean) + qt_ir_commandline_option(perl-identical-output TYPE boolean) + qt_ir_commandline_option(perl-init-check TYPE boolean) + qt_ir_commandline_option(quiet SHORT_NAME q TYPE boolean) + qt_ir_commandline_option(resolve-deps TYPE boolean DEFAULT_VALUE yes) + qt_ir_commandline_option(update TYPE boolean DEFAULT_VALUE yes) + qt_ir_commandline_option(verbose TYPE boolean) +endmacro() + diff --git a/cmake/QtIRParsingHelpers.cmake b/cmake/QtIRParsingHelpers.cmake new file mode 100644 index 00000000..d7d3f20e --- /dev/null +++ b/cmake/QtIRParsingHelpers.cmake @@ -0,0 +1,237 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Retrieves the contents of either .git/config or .gitmodules files for further parsing. +function(qt_ir_get_git_config_contents out_var) + set(options + READ_GITMODULES + READ_GIT_CONFIG + READ_GIT_CONFIG_LOCAL + ) + set(oneValueArgs + WORKING_DIRECTORY + ) + set(multiValueArgs "") + cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(arg_READ_GITMODULES) + set(args -f .gitmodules) + set(file_message ".gitmodules") + elseif(arg_READ_GIT_CONFIG) + set(args "") + set(file_message ".git/config") + elseif(arg_READ_GIT_CONFIG_LOCAL) + set(args "--local") + set(file_message ".local .git/config") + else() + message(FATAL_ERROR "qt_ir_get_git_config_contents: No option specified") + endif() + + qt_ir_get_working_directory_from_arg(working_directory) + + qt_ir_execute_process_and_log_and_handle_error( + FORCE_QUIET + COMMAND_ARGS git config --list ${args} + OUT_OUTPUT_VAR git_output + WORKING_DIRECTORY "${working_directory}" + ERROR_MESSAGE "Failed to get ${file_message} contents for parsing") + + string(STRIP "${git_output}" git_output) + set(${out_var} "${git_output}" PARENT_SCOPE) +endfunction() + +# Checks whether the given url has a scheme like https:// or is just a +# relative path. +function(qt_ir_has_url_scheme url out_var) + string(REGEX MATCH "^[a-z][a-z0-9+\-.]*://" has_url_scheme "${url}") + + if(has_url_scheme) + set(${out_var} TRUE PARENT_SCOPE) + else() + set(${out_var} FALSE PARENT_SCOPE) + endif() +endfunction() + +# Parses a key-value line from a .git/config or .gitmodules file +macro(qt_ir_parse_git_key_value) + string(REGEX REPLACE "^submodule\\.([^.=]+)\\.([^.=]+)=(.*)$" "\\1;\\2;\\3" + parsed_line "${line}") + + list(LENGTH parsed_line parsed_line_length) + set(submodule_name "") + set(key "") + set(value "") + if(parsed_line_length EQUAL 3) + list(GET parsed_line 0 submodule_name) + list(GET parsed_line 1 key) + list(GET parsed_line 2 value) + endif() +endmacro() + +# Parses a url line from a .gitmodules file +# e.g. line - 'submodule.qtbase.url=../qtbase.git' +# +# Arguments +# +# submodule_name +# submodule name, the key in 'submodule.${submodule_name}.url' +# e.g. 'qtbase' +# url_value +# the url where to clone a repo from +# in perl script it was called $base +# e.g. '../qtbase.git', 'https://code.qt.io/playground/qlitehtml.git' +# parent_repo_base_git_path +# the base git path of the parent of the submodule +# it is either a relative dir or a full url +# in the perl script it was called $my_repo_base, +# it was passed as first arg to git_clone_all_submodules, +# it was passed the value of $subbases{$module} when doing recursive submodule cloning +# e.g. 'qt5', 'tqtc-qt5', 'qtdeclarative.git', 'https://code.qt.io/playground/qlitehtml.git' +# +# Outputs +# +# ${out_var_prefix}_${submodule_name}_url +# just the value of ${url_value} +# ${out_var_prefix}_${submodule_name}_base_git_path +# the whole url if it has a scheme, otherwise it's the value of +# ${url_value} relative to ${parent_repo_base_git_path}, so all the ../ are collapsed +# e.g. 'qtdeclarative.git' +# 'https://code.qt.io/playground/qlitehtml.git', +macro(qt_ir_parse_git_url_key out_var_prefix submodule_name url_value parent_repo_base_git_path) + qt_ir_has_url_scheme("${url_value}" has_url_scheme) + if(NOT has_url_scheme) + set(base_git_path "${parent_repo_base_git_path}/${url_value}") + + # The exact code perl code was while ($base =~ s,(?!\.\./)[^/]+/\.\./,,g) {} + # That got rid of ../ and ../../ in the path, but it broke down + # when more than two ../ were present. + # We just use ABSOLUTE to resolve the path and get rid of all ../ + # Note the empty BASE_DIR is important, otherwise the path is relative to + # ${CMAKE_CURRENT_SOURCE_DIR}. + get_filename_component(base_git_path "${base_git_path}" ABSOLUTE BASE_DIR "") + else() + set(base_git_path "${url_value}") + endif() + + set(${out_var_prefix}_${submodule_name}_url "${url_value}" PARENT_SCOPE) + set(${out_var_prefix}_${submodule_name}_base_git_path "${base_git_path}" PARENT_SCOPE) +endmacro() + +# Parses a .git/config or .gitmodules file contents and sets variables for each submodule +# starting with ${out_var_prefix}_ +# These include: +# ${out_var_prefix}_${submodule_name}_path +# the path to the submodule relative to the parent repo +# ${out_var_prefix}_${submodule_name}_branch +# the branch that should be checked out when the branch option is used +# ${out_var_prefix}_${submodule_name}_url +# the url key as encountered in the config +# ${out_var_prefix}_${submodule_name}_base_git_path +# the git base path of the submodule, either a full url or a relative path +# ${out_var_prefix}_${submodule_name}_update +# the status of the submodule, can be 'none' +# ${out_var_prefix}_${submodule_name}_status +# the status of the submodule, can be 'essential', 'addon', etc +# ${out_var_prefix}_${submodule_name}_depends +# the list of submodules that this submodule depends on +# ${out_var_prefix}_${submodule_name}_recommends +# the list of submodules that this submodule recommends to be used with +# ${out_var_prefix}_submodules +# a list of all known submodule names encountered in the file +# ${out_var_prefix}_submodules_to_remove +# a list of all submodules to remove due to update == 'none' +# ${out_var_prefix}_statuses to +# a list of all known submodule statuses like 'essential', 'addon', etc +# ${out_var_prefix}_status_${status}_submodules +# a list of all submodules with the specific status +function(qt_ir_parse_git_config_file_contents out_var_prefix) + set(options + READ_GITMODULES + READ_GIT_CONFIG + READ_GIT_CONFIG_LOCAL + ) + set(oneValueArgs + PARENT_REPO_BASE_GIT_PATH + WORKING_DIRECTORY + ) + set(multiValueArgs "") + cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + qt_ir_get_working_directory_from_arg(working_directory) + + if(NOT arg_PARENT_REPO_BASE_GIT_PATH) + message(FATAL_ERROR + "qt_ir_parse_git_config_file_contents: No base PARENT_REPO_BASE_GIT_PATH specified") + endif() + set(parent_repo_base_git_path "${arg_PARENT_REPO_BASE_GIT_PATH}") + + if(arg_READ_GITMODULES) + set(read_git_config READ_GITMODULES) + elseif(arg_READ_GIT_CONFIG) + set(read_git_config READ_GIT_CONFIG) + elseif(arg_READ_GIT_CONFIG_LOCAL) + set(read_git_config READ_GIT_CONFIG_LOCAL) + else() + message(FATAL_ERROR + "qt_ir_parse_gitmodules_file_contents: No valid git config file specified") + endif() + + qt_ir_get_git_config_contents(contents + ${read_git_config} + WORKING_DIRECTORY "${working_directory}" + ) + string(REPLACE "\n" ";" lines "${contents}") + + set(known_submodules "") + set(statuses "") + set(submodules_to_remove "") + + foreach(line IN LISTS lines) + qt_ir_parse_git_key_value() + if(NOT submodule_name OR NOT key OR value STREQUAL "") + continue() + endif() + + list(APPEND known_submodules "${submodule_name}") + + if(key STREQUAL "path") + set(${out_var_prefix}_${submodule_name}_path "${value}" PARENT_SCOPE) + elseif(key STREQUAL "branch") + set(${out_var_prefix}_${submodule_name}_branch "${value}" PARENT_SCOPE) + elseif(key STREQUAL "url") + qt_ir_parse_git_url_key( + "${out_var_prefix}" "${submodule_name}" "${value}" "${parent_repo_base_git_path}") + elseif(key STREQUAL "update") + # Some repo submodules had a update = none key in their .gitmodules + # in which case we're supposed to skip initialzing those submodules, + # which the perl script did by adding -${submodule_name} to the subset. + # See qtdeclarative Change-Id: I633404f1c00d83dcbdca06a1d287623190323028 + set(${out_var_prefix}_${submodule_name}_update "${value}" PARENT_SCOPE) + if(value STREQUAL "none") + list(APPEND submodules_to_remove "-${submodule_name}") + endif() + elseif(key STREQUAL "status") + set(status_submodules "${${out_var_prefix}_status_${value}_submodules}") + list(APPEND status_submodules "${submodule_name}") + list(REMOVE_DUPLICATES status_submodules) + list(APPEND statuses "${value}") + + set(${out_var_prefix}_status_${value}_submodules "${status_submodules}") + set(${out_var_prefix}_status_${value}_submodules "${status_submodules}" PARENT_SCOPE) + set(${out_var_prefix}_${submodule_name}_status "${value}" PARENT_SCOPE) + elseif(key STREQUAL "depends") + string(REPLACE " " ";" value "${value}") + set(${out_var_prefix}_${submodule_name}_depends "${value}" PARENT_SCOPE) + elseif(key STREQUAL "recommends") + string(REPLACE " " ";" value "${value}") + set(${out_var_prefix}_${submodule_name}_recommends "${value}" PARENT_SCOPE) + endif() + endforeach() + + list(REMOVE_DUPLICATES known_submodules) + list(REMOVE_DUPLICATES submodules_to_remove) + list(REMOVE_DUPLICATES statuses) + set(${out_var_prefix}_submodules "${known_submodules}" PARENT_SCOPE) + set(${out_var_prefix}_submodules_to_remove "${submodules_to_remove}" PARENT_SCOPE) + set(${out_var_prefix}_statuses "${statuses}" PARENT_SCOPE) +endfunction() diff --git a/cmake/QtIRProcessHelpers.cmake b/cmake/QtIRProcessHelpers.cmake new file mode 100644 index 00000000..db7bf2cb --- /dev/null +++ b/cmake/QtIRProcessHelpers.cmake @@ -0,0 +1,165 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# A low-level execute_process wrapper that can be used to execute a single command +# while controlling the verbosity and error handling. +function(qt_ir_execute_process) + set(options + QUIET + ) + set(oneValueArgs + WORKING_DIRECTORY + OUT_RESULT_VAR + OUT_OUTPUT_VAR + OUT_ERROR_VAR + ) + set(multiValueArgs + COMMAND_ARGS + EP_EXTRA_ARGS + ) + cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT arg_COMMAND_ARGS) + message(FATAL_ERROR "Missing arguments to qt_ir_execute_process") + endif() + + if(arg_WORKING_DIRECTORY) + set(working_dir_value "${arg_WORKING_DIRECTORY}") + else() + set(working_dir_value ".") + endif() + set(working_dir WORKING_DIRECTORY "${working_dir_value}") + + set(result_variable "") + if(arg_OUT_RESULT_VAR) + set(result_variable RESULT_VARIABLE proc_result) + endif() + + set(swallow_output "") + if(arg_OUT_OUTPUT_VAR OR arg_QUIET) + list(APPEND swallow_output OUTPUT_VARIABLE proc_output) + endif() + if(arg_OUT_ERROR_VAR OR arg_QUIET) + list(APPEND swallow_output ERROR_VARIABLE proc_error) + endif() + if(NOT arg_QUIET) + set(working_dir_message "") + + qt_ir_is_verbose(verbose) + if(verbose) + set(working_dir_message " current working dir: ") + if(NOT working_dir_value STREQUAL ".") + string(APPEND working_dir_message "${working_dir_value}") + endif() + endif() + + string(REPLACE ";" " " command_args_string "${arg_COMMAND_ARGS}") + message("+ ${command_args_string}${working_dir_message}") + endif() + + execute_process( + COMMAND ${arg_COMMAND_ARGS} + ${working_dir} + ${result_variable} + ${swallow_output} + ${arg_EP_EXTRA_ARGS} + ) + + if(arg_OUT_RESULT_VAR) + set(${arg_OUT_RESULT_VAR} "${proc_result}" PARENT_SCOPE) + endif() + if(arg_OUT_OUTPUT_VAR) + set(${arg_OUT_OUTPUT_VAR} "${proc_output}" PARENT_SCOPE) + endif() + if(arg_OUT_ERROR_VAR) + set(${arg_OUT_ERROR_VAR} "${proc_error}" PARENT_SCOPE) + endif() +endfunction() + +# A higher level execute_process wrapper that can be used to execute a single command +# that is a bit more opinionated and expects options related to init-repository +# functionality. +# It handles queietness, error handling and logging. +# It also allows for slightly more compact syntax for calling processes. +function(qt_ir_execute_process_and_log_and_handle_error) + set(options + NO_HANDLE_ERROR + FORCE_VERBOSE + FORCE_QUIET + ) + set(oneValueArgs + WORKING_DIRECTORY + OUT_RESULT_VAR + OUT_OUTPUT_VAR + OUT_ERROR_VAR + ERROR_MESSAGE + ) + set(multiValueArgs + COMMAND_ARGS + EP_EXTRA_ARGS + ) + cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + qt_ir_get_option_value(quiet quiet) + set(quiet_option "") + if((quiet OR arg_FORCE_QUIET) AND NOT arg_FORCE_VERBOSE) + set(quiet_option "QUIET") + endif() + + set(working_dir "") + if(arg_WORKING_DIRECTORY) + set(working_dir WORKING_DIRECTORY "${arg_WORKING_DIRECTORY}") + endif() + + set(extra_args "") + if(arg_EP_EXTRA_ARGS) + set(extra_args EP_EXTRA_ARGS "${arg_EP_EXTRA_ARGS}") + endif() + + set(out_output_var "") + if(arg_OUT_OUTPUT_VAR OR quiet) + set(out_output_var OUT_OUTPUT_VAR proc_output) + endif() + + set(out_error_var "") + if(arg_OUT_ERROR_VAR OR quiet) + set(out_error_var OUT_ERROR_VAR proc_error) + endif() + + qt_ir_execute_process( + ${quiet_option} + COMMAND_ARGS ${arg_COMMAND_ARGS} + OUT_RESULT_VAR proc_result + ${extra_args} + ${working_dir} + ${out_output_var} + ${out_error_var} + ) + + if(NOT proc_result EQUAL 0 AND NOT arg_NO_HANDLE_ERROR) + set(error_message "") + if(arg_ERROR_MESSAGE) + set(error_message "${arg_ERROR_MESSAGE}\n") + endif() + + string(REPLACE ";" " " cmd "${arg_COMMAND_ARGS}") + string(APPEND error_message "${cmd} exited with status: ${proc_result}\n") + if(proc_output) + string(APPEND error_message "stdout: ${proc_output}\n") + endif() + if(proc_error) + string(APPEND error_message "stderr: ${proc_error}\n") + endif() + message(FATAL_ERROR "${error_message}") + endif() + + if(arg_OUT_RESULT_VAR) + set(${arg_OUT_RESULT_VAR} "${proc_result}" PARENT_SCOPE) + endif() + if(arg_OUT_OUTPUT_VAR) + set(${arg_OUT_OUTPUT_VAR} "${proc_output}" PARENT_SCOPE) + endif() + if(arg_OUT_ERROR_VAR) + set(${arg_OUT_ERROR_VAR} "${proc_error}" PARENT_SCOPE) + endif() +endfunction() diff --git a/cmake/QtIRScript.cmake b/cmake/QtIRScript.cmake new file mode 100644 index 00000000..50cf960c --- /dev/null +++ b/cmake/QtIRScript.cmake @@ -0,0 +1,17 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) + +# Sets up the include paths for all the helpers init-repository uses. +macro(qt_ir_setup_include_paths) + list(APPEND CMAKE_MODULE_PATH + "${CMAKE_CURRENT_LIST_DIR}" + "${CMAKE_CURRENT_LIST_DIR}/3rdparty/cmake" + ) + include(QtIRHelpers) +endmacro() + +qt_ir_setup_include_paths() +qt_ir_include_all_helpers() +qt_ir_run_main_script() diff --git a/cmake/QtTopLevelHelpers.cmake b/cmake/QtTopLevelHelpers.cmake index f8473d72..e84ce757 100644 --- a/cmake/QtTopLevelHelpers.cmake +++ b/cmake/QtTopLevelHelpers.cmake @@ -1,3 +1,6 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + # Populates $out_module_list with all subdirectories that have a CMakeLists.txt file function(qt_internal_find_modules out_module_list) set(module_list "") @@ -15,7 +18,7 @@ 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 depends_file out_dependencies) +function(qt_internal_parse_dependencies_yaml depends_file out_dependencies) file(STRINGS "${depends_file}" lines) set(eof_marker "---EOF---") list(APPEND lines "${eof_marker}") @@ -48,7 +51,7 @@ function(qt_internal_parse_dependencies depends_file out_dependencies) endif() endforeach() message(DEBUG - "qt_internal_parse_dependencies for ${depends_file}\n dependencies: ${dependencies}") + "qt_internal_parse_dependencies_yaml for ${depends_file}\n dependencies: ${dependencies}") set(${out_dependencies} "${dependencies}" PARENT_SCOPE) endfunction() @@ -99,8 +102,22 @@ endfunction() # Keyword arguments: # # PARSED_DEPENDENCIES is a list of dependencies of module in the format that -# qt_internal_parse_dependencies returns. If this argument is not provided, dependencies.yaml of the -# module is parsed. +# 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. # @@ -112,8 +129,9 @@ endfunction() # NORMALIZE_REPO_NAME_IF_NEEDED Will remove 'tqtc-' from the beginning of submodule dependencies # if a tqtc- named directory does not exist. function(qt_internal_resolve_module_dependencies module out_ordered out_revisions) - set(options IN_RECURSION NORMALIZE_REPO_NAME_IF_NEEDED) - set(oneValueArgs REVISION SKIPPED_VAR) + 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) cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) @@ -141,10 +159,42 @@ function(qt_internal_resolve_module_dependencies module out_ordered out_revision if(DEFINED arg_PARSED_DEPENDENCIES) set(dependencies "${arg_PARSED_DEPENDENCIES}") else() - set(depends_file "${CMAKE_CURRENT_SOURCE_DIR}/${module}/dependencies.yaml") set(dependencies "") - if(EXISTS "${depends_file}") - qt_internal_parse_dependencies("${depends_file}" 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() @@ -171,11 +221,24 @@ function(qt_internal_resolve_module_dependencies module out_ordered out_revision 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() + 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} ) if(NOT skipped) list(APPEND ordered ${dep_ordered}) @@ -197,12 +260,30 @@ endfunction() # 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. # # 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 "") + 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 produces. + # 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") @@ -211,6 +292,9 @@ function(qt_internal_sort_module_dependencies modules out_all_ordered) 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} ) # Drop "all_selected_repos" from the output. It depends on all selected repos, thus it must be diff --git a/cmake/QtWriteArgsFile.cmake b/cmake/QtWriteArgsFile.cmake new file mode 100644 index 00000000..336f8550 --- /dev/null +++ b/cmake/QtWriteArgsFile.cmake @@ -0,0 +1,92 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# This script writes its arguments to the file determined by OUT_FILE. +# Each argument appears on a separate line. +# This is used for writing the init-repository.opt file. +# +# This script takes the following arguments: +# IN_FILE: The input file. The whole command line as one string, or one argument per line. +# REDO_FILE: A file containing extra commands to be joined with IN_FILE. +# OUT_FILE: The output file. One argument per line. +# SKIP_ARGS: Number of arguments to skip from the front of the arguments list. +# IGNORE_ARGS: List of arguments to be ignored, i.e. that are not written. +# +# If the REDO_FILE is given, its parameters will be merged with IN_FILE parameters +# and be written into the OUT_FILE. + +cmake_minimum_required(VERSION 3.16) + +# Read arguments from IN_FILE and separate them. +file(READ "${IN_FILE}" raw_args) +# To catch cases where the path ends with an `\`, e.g., `-prefix "C:\Path\"` +string(REPLACE "\\\"" "\"" raw_args "${raw_args}") +string(REPLACE ";" "[[;]]" raw_args "${raw_args}") + +separate_arguments(args NATIVE_COMMAND "${raw_args}") + +string(REPLACE "\;" ";" args "${args}") +string(REPLACE "[[;]]" "\;" args "${args}") + +if(DEFINED REDO_FILE) + file(READ "${REDO_FILE}" raw_redo_args) + separate_arguments(redo_args NATIVE_COMMAND "${raw_redo_args}") + + if(args) + list(FIND args "--" args_ddash_loc) + list(FIND redo_args "--" redo_ddash_loc) + if("${redo_ddash_loc}" STREQUAL "-1") + if("${args_ddash_loc}" STREQUAL "-1") + list(LENGTH args args_ddash_loc) + endif() + # Avoid adding an empty line for an empty -redo + if(NOT "${redo_args}" STREQUAL "") + list(INSERT args ${args_ddash_loc} "${redo_args}") + endif() + else() + # Handling redo's configure options + list(SUBLIST redo_args 0 ${redo_ddash_loc} redo_config_args) + if(redo_config_args) + if("${args_ddash_loc}" STREQUAL "-1") + list(APPEND args "${redo_config_args}") + else() + list(INSERT args ${args_ddash_loc} "${redo_config_args}") + endif() + endif() + + # Handling redo's CMake options + list(LENGTH redo_args redo_args_len) + math(EXPR redo_ddash_loc "${redo_ddash_loc} + 1") + # Catch an unlikely case of -redo being called with an empty --, ie., `-redo --` + if(NOT ${redo_ddash_loc} STREQUAL ${redo_args_len}) + list(SUBLIST redo_args ${redo_ddash_loc} -1 redo_cmake_args) + endif() + + if(DEFINED redo_cmake_args) + if("${args_ddash_loc}" STREQUAL "-1") + list(APPEND args "--") + endif() + list(APPEND args "${redo_cmake_args}") + endif() + endif() + else() + list(APPEND args "${redo_args}") + endif() +endif() + +# Skip arguments if requested +if(DEFINED SKIP_ARGS) + foreach(i RANGE 1 ${SKIP_ARGS}) + list(POP_FRONT args) + endforeach() +endif() + +# Write config.opt +set(content "") +foreach(arg IN LISTS args) + if(NOT arg IN_LIST IGNORE_ARGS) + string(APPEND content "${arg}\n") + endif() +endforeach() + +file(WRITE "${OUT_FILE}" "${content}") diff --git a/init-repository b/init-repository new file mode 100755 index 00000000..8a4469bf --- /dev/null +++ b/init-repository @@ -0,0 +1,24 @@ +#!/bin/sh +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +set -eu + +script_dir_path=`dirname $0` +script_dir_path=`(cd "$script_dir_path"; /bin/pwd)` + +optfile=init-repository.opt +opttmpfile=init-repository.opt.in + +# Posix compatible way to truncate file +: > "$optfile" +: > "$opttmpfile" + +# For consistency, use QtWriteArgsFile.cmake to write the optfile like we do on Windows. +# We do the same with the configure script in qtbase. +for arg in "$@"; do echo \"$arg\" >> "$opttmpfile"; done + +cmake -DIN_FILE="${opttmpfile}" -DOUT_FILE="${optfile}" -P "${script_dir_path}/cmake/QtWriteArgsFile.cmake" + +cmake_script_path="$script_dir_path/cmake/QtIRScript.cmake" +exec cmake -DOPTFILE="${optfile}" -P "$cmake_script_path" diff --git a/init-repository.bat b/init-repository.bat new file mode 100644 index 00000000..b9daca2a --- /dev/null +++ b/init-repository.bat @@ -0,0 +1,23 @@ +:: Copyright (C) 2024 The Qt Company Ltd. +:: SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +@echo off +setlocal ENABLEDELAYEDEXPANSION ENABLEEXTENSIONS +set script_dir_path=%~dp0 +set script_dir_path=%script_dir_path:~0,-1% + +set cmake_scripts_dir=%script_dir_path%\cmake +:: The '.' in 'echo.%*' ensures we don't print "echo is off" when no arguments are passed +:: https://devblogs.microsoft.com/oldnewthing/20170802-00/?p=96735 +:: The space before the '>' makes sure that when we have a digit at the end of the args, we +:: don't accidentally concatenate it with the '>' resulting in '0>' or '2>' which redirects into the +:: file from a stream different than stdout, leading to broken or empty content. +echo.%* >init-repository.opt.in + +call cmake -DIN_FILE=init-repository.opt.in -DOUT_FILE=init-repository.opt ^ + -P "%cmake_scripts_dir%\QtWriteArgsFile.cmake" +call cmake -DOPTFILE=init-repository.opt ^ + -P "%cmake_scripts_dir%\QtIRScript.cmake" + +del init-repository.opt.in +del init-repository.opt