Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Add CONAN_DOWNLOAD and CONAN_ISOLATE_HOME optional features #651

Open
wants to merge 7 commits into
base: develop2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 133 additions & 14 deletions conan_provider.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

set(CONAN_MINIMUM_VERSION 2.0.5)
# Configurable variables
set(CONAN_MINIMUM_VERSION "2.0.5" CACHE STRING "Minimum required Conan version")
set(CONAN_HOST_PROFILE "default;auto-cmake" CACHE STRING "Conan host profile")
set(CONAN_BUILD_PROFILE "default" CACHE STRING "Conan build profile")
set(CONAN_INSTALL_ARGS "--build=missing" CACHE STRING "Command line arguments for conan install")
set(CONAN_DOWNLOAD "if-missing" CACHE STRING "Download the Conan client (always, if-missing or never)")
set(CONAN_DOWNLOAD_VERSION "latest" CACHE STRING "Download a specific Conan version")
set(CONAN_ISOLATE_HOME "if-downloaded" CACHE STRING "Set $CONAN_HOME to \${CMAKE_BINARY_DIR}/conan_home (always, if-downloaded or never)")

# Create a new policy scope and set the minimum required cmake version so the
# features behind a policy setting like if(... IN_LIST ...) behaves as expected
Expand Down Expand Up @@ -456,7 +463,7 @@ function(conan_install)
set(CONAN_OUTPUT_FOLDER ${CMAKE_BINARY_DIR}/conan)
# Invoke "conan install" with the provided arguments
set(CONAN_ARGS ${CONAN_ARGS} -of=${CONAN_OUTPUT_FOLDER})
message(STATUS "CMake-Conan: conan install ${CMAKE_SOURCE_DIR} ${CONAN_ARGS} ${ARGN}")
message(STATUS "CMake-Conan: ${CONAN_COMMAND} install ${CMAKE_SOURCE_DIR} ${CONAN_ARGS} ${ARGN}")


# In case there was not a valid cmake executable in the PATH, we inject the
Expand Down Expand Up @@ -507,7 +514,7 @@ function(conan_get_version conan_command conan_current_version)
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(conan_result)
message(FATAL_ERROR "CMake-Conan: Error when trying to run Conan")
message(FATAL_ERROR "CMake-Conan: Error when trying to run '${conan_command} --version'")
endif()

string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" conan_version ${conan_output})
Expand All @@ -517,24 +524,113 @@ endfunction()

function(conan_version_check)
set(options )
set(oneValueArgs MINIMUM CURRENT)
set(oneValueArgs MINIMUM CURRENT RESULT)
set(multiValueArgs )
cmake_parse_arguments(CONAN_VERSION_CHECK
"${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

if(NOT CONAN_VERSION_CHECK_MINIMUM)
message(FATAL_ERROR "CMake-Conan: Required parameter MINIMUM not set!")
endif()
if(NOT CONAN_VERSION_CHECK_CURRENT)
if(NOT CONAN_VERSION_CHECK_CURRENT)
message(FATAL_ERROR "CMake-Conan: Required parameter CURRENT not set!")
endif()

if(CONAN_VERSION_CHECK_CURRENT VERSION_LESS CONAN_VERSION_CHECK_MINIMUM)
message(FATAL_ERROR "CMake-Conan: Conan version must be ${CONAN_VERSION_CHECK_MINIMUM} or later")
if(CONAN_VERSION_CHECK_RESULT)
set(${CONAN_VERSION_CHECK_RESULT} FALSE PARENT_SCOPE)
endif()
if(CONAN_DOWNLOAD STREQUAL "if-missing")
message(STATUS "CMake-Conan: Found Conan but its version (${CONAN_VERSION_CHECK_CURRENT}) is older than "
"required (${CONAN_VERSION_CHECK_MINIMUM}). Will download the latest version instead.")
else()
message(FATAL_ERROR "CMake-Conan: Conan version must be ${CONAN_VERSION_CHECK_MINIMUM} or later")
endif()
else()
if(CONAN_VERSION_CHECK_RESULT)
set(${CONAN_VERSION_CHECK_RESULT} TRUE PARENT_SCOPE)
endif()
endif()
endfunction()


function(get_latest_conan_version VERSION_VARIABLE)
set(json_file "${CMAKE_BINARY_DIR}/conan_latest_release.json")
foreach(_retry_counter RANGE 3)
file(DOWNLOAD "https://api.github.com/repos/conan-io/conan/releases/latest"
"${json_file}"
INACTIVITY_TIMEOUT 15
STATUS status)
list(GET status 0 status_code)
if(NOT status_code EQUAL 0)
list(GET status 1 message)
message(WARNING "CMake-Conan: Failed to get the latest Conan version info: ${message} (${status_code})")
continue()
endif()
break()
endforeach()
file(READ "${json_file}" json ENCODING UTF-8)
string(REGEX MATCH "\"tag_name\": \"([^\"]+)\"" _ "${json}")
if(NOT _)
message(FATAL_ERROR "CMake-Conan: Failed to parse the latest Conan version info from: '${json_file}'")
endif()
set(${VERSION_VARIABLE} "${CMAKE_MATCH_1}" PARENT_SCOPE)
endfunction()


function(download_conan)
set(options "")
set(oneValueArgs VERSION DESTINATION)
set(multiValueArgs "")
include(CMakeParseArguments)
cmake_parse_arguments(PARSE_ARGV 0 ARG "${options}" "${oneValueArgs}" "${multiValueArgs}")

if(NOT ARG_VERSION)
message(FATAL_ERROR "CMake-Conan: Required parameter VERSION not set!")
endif()
if(NOT ARG_DESTINATION)
message(FATAL_ERROR "CMake-Conan: Required parameter DESTINATION not set!")
endif()

if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "AMD64|amd64|x86_64|x64")
set(HOST_ARCH "x86_64")
elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "X86|i686|i386")
set(HOST_ARCH "i686")
elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64")
set(HOST_ARCH "arm64")
else()
message(FATAL_ERROR "CMake-Conan: Pre-packaged Conan is not available for ${CMAKE_HOST_SYSTEM_PROCESSOR}")
endif()

set(FILE_EXT "tgz")
if(WIN32 AND HOST_ARCH MATCHES "x86_64|i686")
set(HOST_OS "windows")
set(FILE_EXT "zip")
elseif(APPLE AND HOST_ARCH MATCHES "x86_64|arm64")
set(HOST_OS "macos")
elseif(LINUX AND HOST_ARCH STREQUAL "x86_64")
set(HOST_OS "linux")
else()
message(FATAL_ERROR "CMake-Conan: Pre-packaged Conan is not available for ${CMAKE_SYSTEM_NAME} ${CMAKE_HOST_SYSTEM_PROCESSOR}")
endif()

set(CONAN_VERSION ${ARG_VERSION})
set(CONAN_FILE "conan-${CONAN_VERSION}-${HOST_OS}-${HOST_ARCH}.${FILE_EXT}")
set(CONAN_URL "https://github.com/conan-io/conan/releases/download/${CONAN_VERSION}/${CONAN_FILE}")

message(STATUS "CMake-Conan: Downloading Conan ${CONAN_VERSION} from ${CONAN_URL}")
include(FetchContent)
FetchContent_Declare(
Conan
URL "${CONAN_URL}"
DOWNLOAD_DIR ${CMAKE_BINARY_DIR}
SOURCE_DIR "${ARG_DESTINATION}"
DOWNLOAD_EXTRACT_TIMESTAMP 1
)
FetchContent_MakeAvailable(Conan)
endfunction()


macro(construct_profile_argument argument_variable profile_list)
set(${argument_variable} "")
if("${profile_list}" STREQUAL "CONAN_HOST_PROFILE")
Expand All @@ -557,9 +653,37 @@ macro(conan_provide_dependency method package_name)
set_property(GLOBAL PROPERTY CONAN_PROVIDE_DEPENDENCY_INVOKED TRUE)
get_property(_conan_install_success GLOBAL PROPERTY CONAN_INSTALL_SUCCESS)
if(NOT _conan_install_success)
find_program(CONAN_COMMAND "conan" REQUIRED)
conan_get_version(${CONAN_COMMAND} CONAN_CURRENT_VERSION)
conan_version_check(MINIMUM ${CONAN_MINIMUM_VERSION} CURRENT ${CONAN_CURRENT_VERSION})
if(NOT CONAN_DOWNLOAD STREQUAL "always")
find_program(CONAN_COMMAND "conan" QUIET)
if(CONAN_COMMAND)
conan_get_version(${CONAN_COMMAND} CONAN_CURRENT_VERSION)
conan_version_check(MINIMUM ${CONAN_MINIMUM_VERSION} CURRENT ${CONAN_CURRENT_VERSION}
RESULT _conan_version_check_result)
if(NOT _conan_version_check_result)
set(CONAN_COMMAND "-NOTFOUND")
endif()
endif()
endif()
if(NOT CONAN_COMMAND)
if(CONAN_DOWNLOAD STREQUAL "never")
message(FATAL_ERROR "CMake-Conan: Conan executable not found. "
"Please install Conan, set CONAN_COMMAND or enable CONAN_DOWNLOAD")
endif()
if(CONAN_DOWNLOAD_VERSION STREQUAL "latest")
get_latest_conan_version(_download_version)
else()
set(_download_version ${CONAN_DOWNLOAD_VERSION})
endif()
download_conan(VERSION ${_download_version} DESTINATION "${CMAKE_BINARY_DIR}/conan_client")
set(CONAN_COMMAND "${CMAKE_BINARY_DIR}/conan_client/conan")
set(_conan_downloaded TRUE)
endif()

if(CONAN_ISOLATE_HOME STREQUAL "always" OR (CONAN_ISOLATE_HOME STREQUAL "if-downloaded" AND _conan_downloaded))
message(STATUS "CMake-Conan: Setting CONAN_HOME to '${CMAKE_BINARY_DIR}/conan_home'")
set(ENV{CONAN_HOME} "${CMAKE_BINARY_DIR}/conan_home")
endif()

message(STATUS "CMake-Conan: first find_package() found. Installing dependencies with Conan")
if("default" IN_LIST CONAN_HOST_PROFILE OR "default" IN_LIST CONAN_BUILD_PROFILE)
conan_profile_detect_default()
Expand Down Expand Up @@ -660,11 +784,6 @@ endmacro()
# to check if the dependency provider was invoked at all.
cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}" CALL conan_provide_dependency_check)

# Configurable variables for Conan profiles
set(CONAN_HOST_PROFILE "default;auto-cmake" CACHE STRING "Conan host profile")
set(CONAN_BUILD_PROFILE "default" CACHE STRING "Conan build profile")
set(CONAN_INSTALL_ARGS "--build=missing" CACHE STRING "Command line arguments for conan install")

find_program(_cmake_program NAMES cmake NO_PACKAGE_ROOT_PATH NO_CMAKE_PATH NO_CMAKE_ENVIRONMENT_PATH NO_CMAKE_SYSTEM_PATH NO_CMAKE_FIND_ROOT_PATH)
if(NOT _cmake_program)
get_filename_component(PATH_TO_CMAKE_BIN "${CMAKE_COMMAND}" DIRECTORY)
Expand Down
6 changes: 6 additions & 0 deletions tests/resources/download/auto_download/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
cmake_minimum_required(VERSION 3.24)
project(MyApp CXX)

find_package(hello REQUIRED)
add_executable(app main.cpp)
target_link_libraries(app hello::hello)
5 changes: 5 additions & 0 deletions tests/resources/download/auto_download/conanfile.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[requires]
hello/0.1

[generators]
CMakeDeps
6 changes: 6 additions & 0 deletions tests/resources/download/download_function/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
cmake_minimum_required(VERSION 3.24)
project(MyApp CXX)

get_latest_conan_version(LATEST_CONAN_VERSION)
message(STATUS "Latest Conan version: ${LATEST_CONAN_VERSION}")
download_conan(VERSION ${LATEST_CONAN_VERSION} DESTINATION ${CMAKE_BINARY_DIR}/conan)
1 change: 1 addition & 0 deletions tests/resources/download/download_function/conanfile.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[requires]
5 changes: 5 additions & 0 deletions tests/resources/download/get_latest_version/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
cmake_minimum_required(VERSION 3.24)
project(MyApp CXX)

get_latest_conan_version(LATEST_CONAN_VERSION)
message(STATUS "Latest Conan version: ${LATEST_CONAN_VERSION}")
1 change: 1 addition & 0 deletions tests/resources/download/get_latest_version/conanfile.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[requires]
93 changes: 93 additions & 0 deletions tests/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,3 +755,96 @@ def test_try_compile(self, capfd, basic_cmake_project):
run(f'cmake -S {source_dir} -B {binary_dir} -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES={conan_provider}')
out, _ = capfd.readouterr()
assert 'Performing Test HELLO_WORLD_CAN_COMPILE - Success' in out


class TestDownload:
def test_get_latest_version(self, capfd, basic_cmake_project):
source_dir, binary_dir = basic_cmake_project
shutil.copytree(src_dir / 'tests' / 'resources' / 'download' / 'get_latest_version', source_dir, dirs_exist_ok=True)
run(f'cmake -S {source_dir} -B {binary_dir} -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES={conan_provider}')
out, _ = capfd.readouterr()
assert re.search(r'Latest Conan version: \d+\.\d+\.\d+', out)

def test_download_function(self, capfd, basic_cmake_project):
source_dir, binary_dir = basic_cmake_project
shutil.copytree(src_dir / 'tests' / 'resources' / 'download' / 'download_function', source_dir, dirs_exist_ok=True)
run(f'cmake -S {source_dir} -B {binary_dir} -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES={conan_provider}')
out, _ = capfd.readouterr()
assert 'Downloading Conan ' in out
assert (os.path.exists(os.path.join(binary_dir, 'conan', 'conan')) or
os.path.exists(os.path.join(binary_dir, 'conan', 'conan.exe')))

@staticmethod
def _prepare_isolated_home(binary_dir):
# Copy everything except settings.yml from the real CONAN_HOME to the expected isolated CONAN_HOME.
# The settings.yml should be re-created after CMake configure.
conan_home = os.getenv("CONAN_HOME")
isolated_home = os.path.join(binary_dir, "conan_home")
shutil.copytree(conan_home, isolated_home)
isolated_settings_yml = os.path.join(isolated_home, "settings.yml")
os.unlink(isolated_settings_yml)
return isolated_settings_yml

@pytest.mark.parametrize("isolate", ["always", "never"])
def test_isolate_home(self, capfd, basic_cmake_project, isolate):
"Test that CONAN_ISOLATE_HOME=always/never results in {build_dir}/conan_home being used / not used"
source_dir, binary_dir = basic_cmake_project
isolated_settings_yml = self._prepare_isolated_home(binary_dir)
assert not os.path.exists(isolated_settings_yml)
shutil.copytree(src_dir / 'tests' / 'resources' / 'download' / 'auto_download', source_dir, dirs_exist_ok=True)
run(f'cmake -S {source_dir} -B {binary_dir} -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES={conan_provider} -DCMAKE_BUILD_TYPE=Release'
f' -DCONAN_ISOLATE_HOME={isolate}')
out, _ = capfd.readouterr()
if isolate == "always":
assert 'Setting CONAN_HOME to ' in out
assert os.path.exists(isolated_settings_yml)
else:
assert 'Setting CONAN_HOME to ' not in out
assert not os.path.exists(isolated_settings_yml)

@pytest.mark.parametrize("conan_missing", [True, False])
def test_download_never(self, capfd, basic_cmake_project, conan_missing):
source_dir, binary_dir = basic_cmake_project
shutil.copytree(src_dir / 'tests' / 'resources' / 'download' / 'auto_download', source_dir, dirs_exist_ok=True)
run(f'cmake -S {source_dir} -B {binary_dir} -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES={conan_provider} -DCMAKE_BUILD_TYPE=Release'
' -DCONAN_DOWNLOAD=never' + (' -DCONAN_MINIMUM_VERSION=100.0.0' if conan_missing else ''), check=False)
out, err = capfd.readouterr()
assert 'Downloading Conan ' not in out
if conan_missing:
assert 'CMake-Conan: Conan version must be 100.0.0 or later' in err
else:
assert 'CMake-Conan: Conan version must be 100.0.0 or later' not in err

def test_download_always(self, capfd, basic_cmake_project):
source_dir, binary_dir = basic_cmake_project
isolated_settings_yml = self._prepare_isolated_home(binary_dir)
assert not os.path.exists(isolated_settings_yml)
shutil.copytree(src_dir / 'tests' / 'resources' / 'download' / 'auto_download', source_dir, dirs_exist_ok=True)
run(f'cmake -S {source_dir} -B {binary_dir} -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES={conan_provider} -DCMAKE_BUILD_TYPE=Release'
' -DCONAN_DOWNLOAD=always -DCONAN_ISOLATE_HOME=if-downloaded -DCONAN_MINIMUM_VERSION=100.0.0', check=False)
out, _ = capfd.readouterr()
assert 'Downloading Conan ' in out
assert 'Setting CONAN_HOME to ' in out
assert os.path.exists(isolated_settings_yml)
assert (os.path.exists(os.path.join(binary_dir, 'conan_client', 'conan')) or
os.path.exists(os.path.join(binary_dir, 'conan_client', 'conan.exe')))

@pytest.mark.parametrize("conan_missing", [True, False])
def test_download_if_missing(self, capfd, basic_cmake_project, conan_missing):
source_dir, binary_dir = basic_cmake_project
print(binary_dir)
shutil.copytree(src_dir / 'tests' / 'resources' / 'download' / 'auto_download', source_dir, dirs_exist_ok=True)
run(f'cmake -S {source_dir} -B {binary_dir} -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES={conan_provider} -DCMAKE_BUILD_TYPE=Release'
' -DCONAN_DOWNLOAD=if-missing -DCONAN_ISOLATE_HOME=never' + (' -DCONAN_MINIMUM_VERSION=100.0.0' if conan_missing else ''))
out, err = capfd.readouterr()
assert 'CMake-Conan: Conan version must be 100.0.0 or later' not in err
assert not os.path.exists(os.path.join(binary_dir, 'conan_home'))
if conan_missing:
assert 'Will download the latest version instead.' in out
assert 'Downloading Conan ' in out
assert re.search(r"conan_client/conan(\.exe)? install ", out)
assert (os.path.exists(os.path.join(binary_dir, 'conan_client', 'conan')) or
os.path.exists(os.path.join(binary_dir, 'conan_client', 'conan.exe')))
else:
assert 'Will download the latest version instead.' not in out
assert 'Downloading Conan ' not in out
Loading