correct usage of cmake_parse_arguments: specialization of find_package use-case

Hi,

I’m experimenting with a macro find_package in order to add some custom logic to find_package.

I’d like to use cmake_parse_arguments in order to filter the arguments. My first try is like this to get only the components if any:

macro(find_package)

    set(multiValueArgs COMPONENTS)
    cmake_parse_arguments(arg_my_find_package "${multiValueArgs}" ${ARGN})
    ...

But a call such as find_package(pack COMPONENTS comp) gives:

(cmake_parse_arguments):
keyword defined more than once: COMPONENTS

And the variable ${arg_my_find_package_COMPONENTS} is empty.

NB with find_package(pack) ${arg_my_find_package_COMPONENTS} is set to FALSE as expected.

How can I have the expected behavior (getting the COMPONENTS and only them)?

Regards,
A.

You’re actually supposed to pass all arguments to cmake_parse_arguments; it can’t guess what argument type you intended:

    set(options)
    set(oneValueArgs)
    set(multiValueArgs COMPONENTS)
    cmake_parse_arguments(arg_my_find_package
        "${options}" "${oneValueArgs}" "${multiValueArgs}"
        ${ARGN}
    )

For that matter it’s safer to use the PARSE_ARGV form of cmake_parse_arguements; this avoids issues when certain characters such as semicolons are present in the arguments:

    set(options)
    set(oneValueArgs)
    set(multiValueArgs COMPONENTS)
    cmake_parse_arguments(PARSE_ARGV 0 arg_my_find_package
        "${options}" "${oneValueArgs}" "${multiValueArgs}")

For more info, see the docs: https://cmake.org/cmake/help/latest/command/cmake_parse_arguments.html

Do not override built-in commands. I write a blog article discussing this very topic, and another related follow-up article discussing forwarding of command arguments:

2 Likes

Dear Mr Scott,

Thanks for the valuable links.

Regarding first the argument forwarding I’ll have to experiments. I don’t quite understand the official documentation:

Added in version 3.7: The PARSE_ARGV signature is only for use in a function() body. In this case, the arguments that are parsed come from the ARGV# variables of the calling function. The parsing starts with the <N>-th argument, where <N> is an unsigned integer. This allows for the values to have special characters like ; in them.

In what way does it help for arguments containing ;?

Besides, your post explains the dangers of argument forwarding but I don’t get if it gives a solution. Isn’t there, for instance a reciprocal of cmake_parse_arguments?

Regarding specializing find_package I get your point. But I really need this specialization in order to manage ill-packaged libraries (such as OpenCV) or customer-side complex configurations (typically, I’m compiling with different ABI on the same compiler and have to chose the actual path to the dependency according to the project configuration).

At short term, I think I will implement an option to import my specialization or not, document it, mentioning the risks you pinpointed and issuing a CMake warning when used.

Nevertheless, wouldn’t it be useful to have then a documented native_find_package function that implements the standard behavior (and whose redefinition would indeed be “undefined behavior”), that can be used to do such specialization without relying on the underscore trick? It seems really straightforward to implement. Is it worth posting a feature request?

Regards
A.

Thanks,

I didn’t get it straight from the doc first that all the arguments were mandatory.

Regards
A.

It allows you to handle cases like this:

cmake_minimum_required(VERSION 4.1)

function(some_command)
    cmake_parse_arguments(PARSE_ARGV 0 arg "" "" "THINGS")

    foreach(arg IN LISTS arg_THINGS)
        message(STATUS "arg: ${arg}")
    endforeach()
endfunction()

some_command(THINGS aaa "bbb;ccc" ddd)

The output from the above would be:

-- arg: aaa
-- arg: bbb;ccc
-- arg: ddd

Note how the second value after the THINGS keyword contains a semicolon. The quotes around bbb;ccc means the caller wants to treat that as a single argument. The PARSE_ARGV form of cmake_parse_arguments() preserves that. If you used the older, non-PARSE_ARGV form that uses ${ARGN} to provide the arguments to cmake_parse_arguments(), the effect of the quotes is lost, resulting in bbb and ccc being seen as distinct, separate arguments.

I don’t understand your question, sorry. What do you mean by a "reciprocal of cmake_parse_arguments?

The dependency providers feature is the supported way for you to deal with that. Dependency providers allow you to intercept find_package() calls and handle them in whatever way you want. Note though that a dependency provider is meant to be the choice of the user, not the project.

2 Likes

Great! I wasn’t aware of the dependency providers feature. Thank you for having shared your helpful insights and advice. I’m certain it will prove very useful.

Thanks also for the details regarding argument parsing.

Regards,
A.

From CMake documentation:

Only one provider can be set at any point in time. If a provider is already set when cmake_language(SET_DEPENDENCY_PROVIDER) is called, the new provider replaces the previously set one. The specified <command> must already exist when cmake_language(SET_DEPENDENCY_PROVIDER) is called. As a special case, providing an empty string for the <command> and no <methods> will discard any previously set provider.

The dependency provider can only be set while processing one of the files specified by the CMAKE_PROJECT_TOP_LEVEL_INCLUDES variable. Thus, dependency providers can only be set as part of the first call to project(). Calling cmake_language(SET_DEPENDENCY_PROVIDER) outside of that context will result in an error.

I can’t see how you can set a new provider if cmake_language can be called only once. Could you provide an example?

regards
A.

You can call cmake_language(SET_DEPENDENCY_PROVIDER) more than once, but each call is a complete replacement of any earlier such call. The key restriction is that the only place you are allowed to call cmake_language(SET_DEPENDENCY_PROVIDER) is from one of the files listed in CMAKE_PROJECT_TOP_LEVEL_INCLUDES. It’s fine, for example, if there are two files listed in that variable, and each one calls cmake_language(SET_DEPENDENCY_PROVIDER) to set a provider. The result of the first call will be discarded and the second call will be the one that takes effect.

Projects shouldn’t normally set the dependency provider. A CMake preset might set one, but the user is still free to override or not use that. This is all by design, so that users are always in control and projects cannot lock them out of having a choice (whether deliberate or by accident).

2 Likes