Does ENVIRONMENT_MODIFICATION property support generator expression?

I’m using set_tests_properties to prepend a new directory in $ENV{PATH}. What I expect is this test case can find a imported target shared library.

set_tests_properties(normal_test
  PROPERTIES
    ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<PATH:GET_PARENT_PATH,$<SHELL_PATH:$<TARGET_PROPERTY:aaa,IMPORTED_LOCATION$<$<CONFIG:Debug>:_DEBUG>>>>"
)

The aaa is just a imported target name whose IMPORTED_LOCATION and IMPORTED_LOCATION_DEBUG set to the relevant shared library.

But it does not work as I want it to.

I try to use add_custom_target and ${CMAKE_COMMAND} -E echo to print that generator expression value. It’s correct.

So my question shows as title. And if not what is the right way to do it?

This issue is relevant to your query. How are you defining your tests? Are you using the add_test(NAME) form?

Yes. add_test(NAME normal_test COMMAND).

Please describe more fully what happens and how that is not working. It would also help if you provided a minimal, complete example that demonstrates the problem. Without that, it’s likely we’ll miss the underlying cause and not be able to work out what’s going on for you.

What I want to do is let my test case can find the imported target shared library by adding imported target shared library path to PATH environment variable. I use ENVIRONMENT_MODIFICATION and path_list_prepend to modify environment variable and $<TARGET_PROPERTY:bbb::bbb,IMPORTED_LOCATION$<$<CONFIG:Debug>:_DEBUG> to get shared library full path in different build configuration.

  1. I add a test case using add_test(NAME normal_test COMMAND) for executable target aaa.
add_test(NAME aaa_normal_test
  COMMAND aaa "${CMAKE_CURRENT_LIST_DIR}/test.data")
  1. I modify env by set_tests_properties.
set_tests_properties (aaa_normal_test
  PROPERTIES
    ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<PATH:GET_PARENT_PATH,$<SHELL_PATH:$<TARGET_PROPERTY:bbb::bbb,IMPORTED_LOCATION$<$<CONFIG:Debug>:_DEBUG>>>>"
)
  1. The aaa is just a executable target.
add_executable (aaa
  "main.cpp"
)
  1. The bbb::bbb is an alias target.
add_library (bbb SHARED IMPORTED GLOBAL)

set_target_properties (bbb
  IMPORTED_LOCATION "${CMAKE_CURRENT_LIST_DIR}/lib/Release/x64/bbb.dll"
  IMPORTED_IMPLIB "${CMAKE_CURRENT_LIST_DIR}/lib/Release/x64/bbb.lib"
  IMPORTED_LOCATION_DEBUG "${CMAKE_CURRENT_LIST_DIR}/lib/Debug/x64/bbb.dll"
  IMPORTED_IMPLIB "${CMAKE_CURRENT_LIST_DIR}/lib/Debug/x64/bbb.lib"
  IMPORTED_CONFIGURATIONS "Release;Debug"
  MAP_IMPORTED_CONFIG_RELWITHDEBUGINFO Release
  MAP_IMPORTED_CONFIG_MINSIZEREL Release
)

target_include_directories (bbb
  INTERFACE "${CMAKE_CURRENT_LIST_DIR}/include"
)

add_library (bbb::bbb ALIAS bbb)

install (IMPORTED_RUNTIME_ARTIFACTS bbb)
  1. I also linked aaa with bbb.
target_link_libraries (aaa
  PRIVATE bbb::bbb
)

I expects that the test case should execute normally at least no matter passing or failing. But it says “can not find library bbb.dll” on Windows. I use procexp64.exe from SysinternalsSuite to check PATH and the imported target shared library path does not show up.

So, does generate expression support in ENVIRONMENT_MODIFICATION? And It is the way to use a pre-built shared library on Windows? What if not, how to do it?

Thanks for the details, but what I was after was a complete minimal example, i.e. a full CMakeLists.txt file and any source files it needs. Then I can (a) verify I get the same problem as you, (b) confirm that there’s not some hidden detail affecting the behavior.

From what you’ve shown, I can see this generator expression is malformed:

set_tests_properties (aaa_normal_test
  PROPERTIES
    ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<PATH:GET_PARENT_PATH,$<SHELL_PATH:$<TARGET_PROPERTY:bbb::bbb,IMPORTED_LOCATION$<$<CONFIG:Debug>:_DEBUG>>>>"
)

The $<PATH:GET_PARENT_PATH,...> expression expects its argument to be in CMake’s path format, which is always forward slashes. But you’re providing a native shell path to it by using $<SHELL_PATH:...>. You need to reverse the order you apply those operations.

set_tests_properties (aaa_normal_test
  PROPERTIES
    ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<SHELL_PATH:$<PATH:GET_PARENT_PATH,$<TARGET_PROPERTY:bbb::bbb,IMPORTED_LOCATION$<$<CONFIG:Debug>:_DEBUG>>>>"
)

You’re also assuming that IMPORTED_LOCATION_DEBUG is set when using the Debug config, but otherwise the IMPORTED_LOCATION is set. One of these might not be true. It is more common for imported targets to either set just IMPORTED_LOCATION, or to only set the IMPORTED_LOCATION_xxx config-specific properties. It is much less common to set both. EDIT: I didn’t read far enough, I can see that you are setting the properties on bbb:bbb too, and they are set consistent with how you’re using them.

I made a test project to let you can reproduce it.

  1. project tree
├─test
│  │  CMakeLists.txt
│  │  main.cpp
│  │
│  └─bbb
│          CMakeLists.txt
│
└─test_shared_lib
        CMakeLists.txt
        shared_lib_main.cpp
        shared_lib_main.h
  1. steps to produce

  2. build shared library

run commands to build shard library

cd ${SHARED_LIBRARY_ROOT}
cmake -S . -B build
cmake --build build --config Debug
cmake --install build --prefix install/Debug --config Debug
cmake --build build --config Release
cmake --install build --prefix install/Release --config Release
  1. copy output to test dir

copy all content in install directory to test/bbb

and then copy test/bbb/Relase/include or test/bbb/Relase/include to test/bbb

  1. build test binary and run ctest

If all proceduces above are correct, the building should success.

use visual studio to open test CMakeLists.txt and then build it.

run tests from TestExplorer.

  1. files
  • test/CMakeLists.txt
# test
#

cmake_minimum_required (VERSION 3.28)

project (test_project 
  VERSION 0.0.1
  LANGUAGES C CXX
)

add_subdirectory ("bbb")

include (CTest)

add_executable (test_main
  "main.cpp"
)

target_compile_features (test_main
  PRIVATE cxx_std_23
)

target_link_libraries (test_main
  PRIVATE bbb::bbb
)

if (${BUILD_TESTING})
  add_test (NAME normal_function_test_test_main
    COMMAND test_main
  )

  set_tests_properties (normal_function_test_test_main
    PROPERTIES 
	  ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<SHELL_PATH:$<PATH:GET_PARENT_PATH,$<TARGET_PROPERTY:bbb::bbb,IMPORTED_LOCATION$<IF:$<CONFIG:Debug>,_DEBUG,_RELEASE>>>>" 
  )
endif()
  • test/main.cpp
// main.cpp
//

#include <shared_lib_main.h>

int main(int argc, char* argv[])
{
  int out = 0;
  int res = safe_add_two_integer(1, 2, &out);

  if (res == 0)
  {
    return 1;
  }
  return 0;
}
  • bbb/CMakeLists.txt
# bbb
#

add_library (bbb SHARED IMPORTED GLOBAL)

set_target_properties (bbb PROPERTIES
  IMPORTED_LOCATION_RELEASE "${CMAKE_CURRENT_LIST_DIR}/Release/lib/test_shared_lib.dll"
  IMPORTED_IMPLIB_RELEASE "${CMAKE_CURRENT_LIST_DIR}/Release/lib/test_shared_lib.lib"
  IMPORTED_LOCATION_DEBUG "${CMAKE_CURRENT_LIST_DIR}/Debug/lib/test_shared_lib.dll"
  IMPORTED_IMPLIB_DEBUG "${CMAKE_CURRENT_LIST_DIR}/Debug/lib/test_shared_lib.lib"
  IMPORTED_CONFIGURATIONS "Release;Debug"
  MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release
  MAP_IMPORTED_CONFIG_MINSIZEREL Release
)

target_include_directories (bbb 
  INTERFACE "${CMAKE_CURRENT_LIST_DIR}/include"
)

add_library (bbb::bbb ALIAS bbb)

install (IMPORTED_RUNTIME_ARTIFACTS bbb)
  • test_shared_lib/CMakeLists.txt
# test
#

# build and install commands:
# > cd ${PROJECT_ROOT}
# > cmake -S . -B build
# > cmake --build build --config Debug
# > cmake --install build --prefix install/Debug --config Debug
# > cmake --build build --config Release
# > cmake --install build --prefix install/Release --config Release

cmake_minimum_required (VERSION 3.23)

project (test_shared_lib_project 
  VERSION 0.0.1
  LANGUAGES C CXX
)

include (GenerateExportHeader)

add_library (test_shared_lib SHARED)

set_target_properties (test_shared_lib
  PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON
)

generate_export_header (test_shared_lib)

target_sources (test_shared_lib
  PRIVATE "shared_lib_main.cpp"
  PUBLIC FILE_SET public_headers TYPE HEADERS FILES "shared_lib_main.h"
)

install (TARGETS test_shared_lib 
  RUNTIME DESTINATION lib
  LIBRARY DESTINATION lib
  ARCHIVE DESTINATION lib
  FILE_SET public_headers DESTINATION include
)
install (FILES "${PROJECT_BINARY_DIR}/test_shared_lib_export.h" TYPE INCLUDE)
  • test_shared_lib/shared_lib_main.cpp
// main.cpp
//

#ifdef __cplusplus
extern "C" {
#endif // __cplusplus

int safe_add_two_integer(int a, int b, int* out)
{
  if (out == nullptr)
  {
    return 0;
  }

  if (a > ~b)
  {
    return 0;
  }

  *out = a + b;

  return 1;
}

#ifdef __cplusplus
}
#endif // __cplusplus
  • test_shared_lib/shread_lib_main.h
// shared_lib_main.h
//

#pragma once

#include "test_shared_lib_export.h"

#ifdef __cplusplus
extern "C" {
#endif // __cplusplus

int TEST_SHARED_LIB_EXPORT safe_add_two_integer(int a, int b, int* out);

#ifdef __cplusplus
}
#endif // __cplusplus

Thanks for your patience, it took me a while to find time to investigate this one.

The problem is due to your project, not the generator expression functionality. The way you’ve handled the symbol export has some errors. Firstly, you’re explicitly handling symbol visibility by using generate_export_header(), which is good. But then you also set the WINDOWS_EXPORT_ALL_SYMBOLS target property. Don’t set that property, you don’t need it and I wouldn’t be surprised if that is interfering with things.

The next error is you’ve misplaced the TEST_SHARED_LIB_EXPORT define in the shared_lib_main.h header. It should be this:

TEST_SHARED_LIB_EXPORT int safe_add_two_integer(int a, int b, int* out);

Note the TEST_SHARED_LIB_EXPORT comes before the int return type.

The main error though is that your shared_lib_main.cpp file doesn’t include the shared_lib_main.h header. As a consequence, when the compiler compiles shared_lib_main.cpp, it doesn’t see the symbol visibility annotation and the safe_add_two_integer() function doesn’t end up being exported in the resultant binary. Once you add a #include <test_shared_lib.h> to your shared_lib_main.cpp, you’ll then get an error that it can’t find test_shared_lib_export.h. You’ll need to add a line like the following to fix that:

target_include_directories(test_shared_lib PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>)

Switching to your test project, the main.cpp there will return an exit code of 1 as things are currently written. Therefore, when you run ctest, the test will fail, and it will be hard to distinguish that expected scenario from a situation where there was a problem starting the executable. I had to add some debug output and make it return 0 in all cases to be able to diagnose this one.

You can also verify that the PATH handling is correct by replacing your test executable with a command that runs a CMake script, and have that CMake script print out the value of $ENV{PATH}. When I did this, I was able to confirm the PATH was what we expect.